Map/TableColumn.js

/*global require*/
"use strict";

var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;
var destroyObject = require("terriajs-cesium/Source/Core/destroyObject")
  .default;
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
  .default;
var JulianDate = require("terriajs-cesium/Source/Core/JulianDate").default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var uniq = require("lodash.uniq");

var formatNumberForLocale = require("../Core/formatNumberForLocale");
var inherit = require("../Core/inherit");
var VarType = require("../Map/VarType");
var VarSubType = require("../Map/VarSubType");
var VariableConcept = require("../Map/VariableConcept");

var typeHintSet = [
  { hint: /^(lon|long|longitude|lng)$/i, type: VarType.LON },
  { hint: /^(lat|latitude)$/i, type: VarType.LAT },
  { hint: /^(address|addr)$/i, type: VarType.ADDR },
  { hint: /^(.*[_ ])?(depth|height|elevation)$/i, type: VarType.ALT },
  { hint: /^(.*[_ ])?(time|date)/i, type: VarType.TIME }, // Quite general, eg. matches "Start date (AEST)".
  { hint: /^(year)$/i, type: VarType.TIME }, // Match "year" only, not "Final year" or "0-4 years".
  { hint: /^postcode|poa|(.*_code)$/i, type: VarType.ENUM }
];

var endDateHintSet = [
  { hint: /^(.*[_ ])?end[\s_]?(time|date)/i, type: true } // Matches "end_date" or "My end time (AEST)".
];

var subtypeHintSet = [{ hint: /^(.*[_ ])?(year)/i, type: VarSubType.YEAR }];

var defaultReplaceWithNullValues = ["na", "NA", "-"];
var defaultReplaceWithZeroValues = [];

/**
 * TableColumn is a light class containing a single variable (or column) from a TableStructure.
 * It guesses the variable type (time, enum etc) from the variable name.
 * It extends VariableConcept, which is used to represent the variable in the NowViewing tab.
 * This gives it isActive, isSelected and color fields.
 * In future it may perform additional processing.
 *
 * @alias TableColumn
 * @constructor
 * @extends {VariableConcept}
 * @param {String} [name] The name of the variable.
 * @param {Number[]} [values] An array of values for the variable.
 * @param {Object} [options] Options:
 * @param {Boolean} [options.active] Whether the variable should start active.
 * @param {TableStructure} [options.tableStructure] The table structure this column belongs to. Required so that only one column is selected at a time.
 * @param {VarType} [options.type] The variable type (eg. VarType.TIME). If not present, an educated guess is made based on the name and values.
 * @param {VarSubType} [options.subtype] The variable subtype (eg. VarSubType.YEAR). If not present, an educated guess is made based on the name and values.
 * @param {Boolean} [options.isEndDate] True if this is has type time and is an end_date. If not present, an educated guess is made based on the name and values.
 * @param {VarType[]} [options.unallowedTypes] An array of types which should not be guessed. If not present, all types are allowed. Cannot include VarType.SCALAR.
 * @param {VarType[]} [options.displayVariableTypes] If present, only make this variable visible if its type is in this list.
 * @param {String[]} [options.replaceWithNullValues] If present, and this is a SCALAR type with at least one numerical value, then replace these values with null.
 *        Defaults to ['na', 'NA'].
 * @param {String[]} [options.replaceWithZeroValues] If present, and this is a SCALAR type with at least one numerical value, then replace these values with 0.
 *        Defaults to [null, '-']. (Blank values like '' are converted to null before they reach here, so use null instead of '' to catch missing values.)
 * @param {Number} [options.displayDuration]
 * @param {String} [options.id] Provided so that columns can be renamed; their original name is stored as the id.
 * @param {String} [options.format] A format string for this column. For numbers, this is passed as options to toLocaleString.
 * @param {String} [options.units] The units of this column, if known. Not currently used internally by TableStructure or TableColumn.
 * @param {String} [options.chartLineColor] The string description of the chart line color of this variable, if any.
 * @param {Number} [options.yAxisMin] Override for the minimum display value of the y axis in charts.
 * @param {Number} [options.yAxisMax] Override for the maximum display value of the y axis in charts.
 */
var TableColumn = function(name, values, options) {
  this.options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  // Note - if you add more options, be sure to include them in getFullOptions() too.
  VariableConcept.call(this, name, {
    parent: this.options.tableStructure,
    active: this.options.active,
    color: this.options.chartLineColor
  });
  this.id = defaultValue(this.options.id, this.id); // if options.id is provided, use it to override the default (this.id = this.name).
  this.format = this.options.format;
  this.units = this.options.units;

  this._rawValues = values;
  this._unallowedTypes = defaultValue(this.options.unallowedTypes, []);
  this._replaceWithZeroValues = defaultValue(
    this.options.replaceWithZeroValues,
    defaultReplaceWithZeroValues
  );
  this._replaceWithNullValues = defaultValue(
    this.options.replaceWithNullValues,
    defaultReplaceWithNullValues
  );
  this._type = this.options.type;
  this._subtype = this.options.subtype;
  this._isEndDate = this.options.isEndDate;
  if (!defined(this._type)) {
    this.setTypeAndSubTypeFromName();
  }

  var isNumerical = function(value) {
    return typeof value === "number";
  };

  if (this._type === VarType.SCALAR && values.some(isNumerical)) {
    // Before setting this._values, replace '-' and 'NA' etc with zero/null. Min/max values ignore nulls.
    this._values = replaceValues(
      values,
      this._replaceWithZeroValues,
      this._replaceWithNullValues
    );
  } else {
    this._values = values;
  }
  this._numericalValues = this._values && this._values.filter(isNumerical);

  var nonNullValues = this._values.filter(function(value) {
    return value !== null;
  });
  this._minimumValue = Math.min.apply(null, nonNullValues); // Note: a single NaN value makes this NaN (hence replaceValues above).
  this._maximumValue = Math.max.apply(null, nonNullValues);

  this.yAxisMin = this.options.yAxisMin;
  this.yAxisMax = this.options.yAxisMax;

  this._uniqueValues = undefined;
  this._indicesIntoUniqueValues = undefined;

  this.displayDuration = this.options.displayDuration; // undefined is fine.

  /**
   * this.dates is a version of values that has been converted to javascript Dates.
   * Only if type === VarType.TIME.
   */
  this.dates = undefined;
  /**
   * this.julianDates is a version of values that has been converted to JulianDates.
   * Only if type === VarType.TIME.
   */
  this.julianDates = undefined;
  /**
   * this.finishJulianDates is an Array of JulianDates listing the next different date in the values array, less 1 second.
   * This is populated by TableStructure, since it may depend on other columns.
   * Only if type === VarType.TIME.
   */
  this.finishJulianDates = undefined;
  /**
   * A TimeInterval Array giving when each row applies.
   * This is populated by TableStructure, since it may depend on other columns.
   * Only if type === VarType.TIME.
   */
  this._timeIntervals = undefined;
  /**
   * A DataSourceClock whose start and stop times correspond to the first and last visible row.
   * This is populated by TableStructure, since it may depend on other columns.
   * Only if type === VarType.TIME.
   */
  this._clock = undefined;

  if (defined(values) && this._type === VarType.TIME) {
    var jsDatesAndJulianDates = convertToDates(this);
    this.dates = jsDatesAndJulianDates.jsDates;
    this.julianDates = jsDatesAndJulianDates.julianDates;
    if (this.dates.length === 0) {
      // We couldn't interpret this as dates after all. Change type to scalar.
      this._type = VarType.SCALAR;
    } else {
      this._subtype = jsDatesAndJulianDates.subtype;
    }
  }

  // If it looked like a SCALAR but there are no numerical values, change type to ENUM.
  if (isNaN(this._minimumValue) && this._type === VarType.SCALAR) {
    this._type = VarType.ENUM;
  }

  // Finally, distinguish between ENUM and html tags.
  if (this._type === VarType.ENUM && looksLikeHtmlTags(this.values)) {
    this._type = VarType.TAG;
  }

  updateForType(this);

  this._formattedValues = getFormattedValues(this);

  // Track _type so that TableStructure can change columnsByType if type changes.
  // Track _values so that charts can update live with new data.
  // Track units so that we can set the units after data has loaded, and the chart panel updates.
  knockout.track(this, ["_type", "_values", "units", "_timeIntervals"]);
};

inherit(VariableConcept, TableColumn);

function replaceValues(values, replaceWithZeroValues, replaceWithNullValues) {
  // Replace "bad" values like "-" with zero, and "na" with null.
  // Note this does not go back and update TableStructure._rows, so the row descriptions will still show the original values.
  return values.map(function(value) {
    if (replaceWithZeroValues.indexOf(value) >= 0) {
      return 0;
    }
    if (replaceWithNullValues.indexOf(value) >= 0) {
      return null;
    }
    return value;
  });
}

function updateForType(tableColumn) {
  // Currently cannot change type to TIME and expect it to work.
  // But could update this.dates etc when set to VarType.TIME (if needed).
  tableColumn._uniqueValues = undefined;
  tableColumn._indicesIntoUniqueValues = undefined;

  tableColumn._displayVariableTypes = tableColumn.options.displayVariableTypes;
  if (defined(tableColumn._displayVariableTypes)) {
    tableColumn.isVisible =
      tableColumn._displayVariableTypes.indexOf(tableColumn._type) >= 0;
  }
}

Object.defineProperties(TableColumn.prototype, {
  /**
   * Gets or sets the type of this column.
   * @memberOf TableColumn.prototype
   * @type {VarType}
   */
  type: {
    get: function() {
      return this._type;
    },
    set: function(type) {
      this._type = type;
      updateForType(this);
    }
  },

  /**
   * Gets or sets the subtype of this column.
   * @memberOf TableColumn.prototype
   * @type {VarSubType}
   */
  subtype: {
    get: function() {
      return this._subtype;
    },
    set: function(subtype) {
      this._subtype = subtype;
      // updateForType(this);
    }
  },

  /**
   * Gets the values of this column.
   * @memberOf TableColumn.prototype
   * @type {Array}
   */
  values: {
    get: function() {
      return this._values;
    }
  },

  /**
   * Gets the column's numerical values only.
   * This is the quantity used for the legend.
   * @memberOf TableColumn.prototype
   * @type {Array}
   */
  numericalValues: {
    get: function() {
      return this._numericalValues;
    }
  },

  /**
   * Returns whether this column is an ENUM type.
   * @memberOf TableColumn.prototype
   * @type {Boolean}
   */
  isEnum: {
    get: function() {
      return this._type === VarType.ENUM;
    }
  },

  /**
   * Gets formatted values of this column.
   * @memberOf TableColumn.prototype
   * @type {Array}
   */
  formattedValues: {
    get: function() {
      return this._formattedValues;
    }
  },

  /**
   * Gets the minimum value of this column.
   * @memberOf TableColumn.prototype
   * @type {Number}
   */
  minimumValue: {
    get: function() {
      return this._minimumValue;
    }
  },

  /**
   * Gets the maximum value of this column.
   * @memberOf TableColumn.prototype
   * @type {Number}
   */
  maximumValue: {
    get: function() {
      return this._maximumValue;
    }
  },

  /**
   * Returns this column's unique values only. Only defined if non-numeric.
   * @memberOf TableColumn.prototype
   * @type {Array}
   */
  uniqueValues: {
    get: function() {
      if (this.isEnum && !defined(this._uniqueValues)) {
        this._uniqueValues = uniq(this._values).filter(function(value) {
          return value !== null;
        });
        sortMostCommonFirst(this._values, this._uniqueValues);
      }
      return this._uniqueValues;
    }
  },

  /**
   * Returns this column's values, except for TIME-type columns, in which case the julian dates are returned.
   * @memberOf TableColumn.prototype
   * @type {Array}
   */
  julianDatesOrValues: {
    get: function() {
      return this.type === VarType.TIME ? this.julianDates : this._values;
    }
  },

  /**
   * Returns an array describing when each row is visible. Only defined if type == VarType.TIME.
   * @memberOf TableColumn.prototype
   * @type {TimeIntervalCollection[]}
   */
  timeIntervals: {
    get: function() {
      return this._timeIntervals;
    }
  },

  /**
   * Returns a clock whose start and stop times correspond to the first and last visible row.
   * Only defined if type == VarType.TIME.
   * @memberOf TableColumn.prototype
   * @type {DataSourceClock}
   */
  clock: {
    get: function() {
      return this._clock;
    }
  }
});

// If -'s or /'s are used to separate the fields, replace them with /'s, and
// swap the first and second fields.
// Eg. '30-12-2015' => '12/30/2015', the US format, because that is what javascript's Date expects.
function swapDateFormat(v) {
  var part = v.split(/[/-]/);
  if (part.length === 3) {
    v = part[1] + "/" + part[0] + "/" + part[2];
  }
  return v;
}

// Replace hypens with slashes in a three-part date, eg. '4-6-2015' => '4/6/2015' or '2015-12-5' => '2015/12/5'.
// This helps because '2015-12-5' will display differently in different browsers, whereas '2015/12/5' will not.
// Also, convert timestamp info, dropping milliseconds, timezone and replacing 'T' with a space.
// Eg.: 'yyyy-mm-ddThh:mm:ss.qqqqZ' => 'yyyy/mm/dd hh:mm:ss'.
function replaceHyphensAndConvertTime(v) {
  var time = "";
  if (!defined(v.indexOf)) {
    // could be a number, eg. times may be simple numbers like 730.
    return v;
  }
  var tIndex = v.indexOf("T");
  if (tIndex >= 0) {
    var times = v.substr(tIndex + 1).split(":");
    if (times && times.length > 1) {
      time = " " + times[0] + ":" + times[1];
    }
    if (times.length > 2) {
      time = time + ":" + parseInt(times[2], 10);
    }
    v = v.substr(0, tIndex);
  }
  var part = v.split(/-/);
  if (part.length === 3) {
    v = part[0] + "/" + part[1] + "/" + part[2];
  }
  return v + time;
}

function isInteger(value) {
  // Eg. Returns false for '99a', undefined and null, true for '99' and 99.
  return (
    !isNaN(value) &&
    parseInt(Number(value), 10) === +value &&
    !isNaN(parseInt(value, 10))
  );
}

/**
 * Returns the options you would pass to recreate this column.
 * @return {Object} An options parameter suitable for passing to new TableColumn().
 */
TableColumn.prototype.getFullOptions = function() {
  return {
    tableStructure: this.parent,
    active: this.isActive,
    id: this.id,
    format: this.format,
    units: this.units,
    unallowedTypes: this._unallowedTypes,
    replaceWithZeroValues: this._replaceWithZeroValues,
    replaceWithNullValues: this._replaceWithNullValues,
    type: this._type,
    subtype: this._subtype,
    isEndDate: this._isEndDate,
    displayDuration: this.displayDuration,
    displayVariableTypes: this._displayVariableTypes,
    chartLineColor: this.color,
    yAxisMin: this.yAxisMin,
    yAxisMax: this.yAxisMax
  };
};

/**
 * Simple check to try to guess date format, based on max value of first position.
 * If dates are consistent with US format, it will use US format (mm-dd-yyyy).
 *
 * @param {Array} goodValues An array of the column values, with any bad (eg. null) values removed.
 * @param {Integer} [subtype] If known, eg. VarSubType.YEAR.
 * @private
 * @return {Object} Object with keys:
 *         subtype: The identified subtype, or undefined.
 *         jsDates: The values as javascript dates.
 *         julianDates: The values as JulianDates.
 */
TableColumn.convertToDates = function(goodValues, subtype) {
  // All browsers appear to understand both yyyy/m/d and m/d/yyyy as arguments to Date (but not with hyphens).
  // See http://dygraphs.com/date-formats.html
  var firstPositionMaximum = 0; // call this firstPositionMaximum because parseInt('12-10') = 12.
  goodValues.forEach(function(value) {
    var firstPosition = parseInt(value, 10);
    if (firstPosition > firstPositionMaximum) {
      firstPositionMaximum = firstPosition;
    }
  });

  var dateParsers; // returns [jsDate, julianDate].
  // First, could it be a simple integer year format? Assume if all integers less than 9999, then years.
  if (
    subtype === VarSubType.YEAR ||
    (firstPositionMaximum < 9999 &&
      goodValues.every(value => isInteger(value) || !defined(value)))
  ) {
    // It's ok to have some missing (null or undefined) values.
    subtype = VarSubType.YEAR;
    dateParsers = function(v) {
      var jsDate = new Date(v + "/01/01");
      return [jsDate, JulianDate.fromDate(jsDate)];
    };
  } else if (
    firstPositionMaximum < 9999 &&
    goodValues.every(
      value =>
        !defined(value) || (defined(value.indexOf) && value.indexOf("-Q")) === 4
    )
  ) {
    // Is it quarterly data in the format yyyy-Qx ? (Ignoring null values, and failing on any purely numeric values)
    dateParsers = function(v) {
      var year = v.slice(0, 4);
      var quarter = v.slice(6);
      var monthString;
      if (quarter === "1") {
        monthString = "01/01";
      } else if (quarter === "2") {
        monthString = "04/01";
      } else if (quarter === "3") {
        monthString = "07/01";
      } else if (quarter === "4") {
        monthString = "10/01";
      } else {
        return [undefined, undefined];
      }
      var jsDate = new Date(year + "/" + monthString);
      return [jsDate, JulianDate.fromDate(jsDate)];
    };
  } else if (firstPositionMaximum > 31) {
    dateParsers = function(v) {
      // If it contains a space, it may be either yyyy-mm-dd hh:mm:ss, or yyyy/mm/dd hh:mm:ss.
      if (v.indexOf(" ") > 0 && v.indexOf(":") > 0) {
        var jsDate = new Date(replaceHyphensAndConvertTime(v));
        return [jsDate, JulianDate.fromDate(jsDate)];
      } else {
        // Assume it is a properly defined ISO format  yyyy-mm-dd or yyyy-mm-ddThh:mm:ss
        // Note that Safari and some older browsers cannot handle ISO format, hence the need to go via JulianDate.
        var julianDate = JulianDate.fromIso8601(v);
        return [JulianDate.toDate(julianDate), julianDate]; // It may be better to use jsDate = new Date(replaceHyphensAndConvertTime(v));
      }
    };
  } else if (firstPositionMaximum > 12) {
    //Int'l javascript format dd-mm-yyyy
    dateParsers = function(v) {
      var jsDate = new Date(swapDateFormat(v));
      return [jsDate, JulianDate.fromDate(jsDate)];
    };
  } else {
    //USA javascript date format mm-dd-yyyy
    dateParsers = function(v) {
      var jsDate = new Date(replaceHyphensAndConvertTime(v)); // The T check is overkill for this.
      return [jsDate, JulianDate.fromDate(jsDate)];
    };
  }

  var results = [];
  try {
    results = goodValues.map(function(v) {
      if (defined(v)) {
        const parsed = dateParsers(v);
        if (
          !isFinite(parsed[0]) ||
          !defined(parsed[1]) ||
          !isFinite(parsed[1].dayNumber) ||
          !isFinite(parsed[1].secondsOfDay)
        ) {
          return [undefined, undefined];
        } else {
          return parsed;
        }
      } else {
        return [undefined, undefined];
      }
    });
  } catch (err) {
    // Repeat one by one so we can display the bad date.
    try {
      for (var i = 0; i < goodValues.length; i++) {
        dateParsers(goodValues[i]);
      }
    } catch (err) {
      console.log("Unable to parse date:", goodValues[i], err);
    }
  }
  // We now have results = [ [jsDate1, julianDate1], [jsDate2, julianDate2], ...] - unzip them and return them.
  return {
    subtype: subtype,
    jsDates: results.map(function(twoDates) {
      return twoDates[0];
    }),
    julianDates: results.map(function(twoDates) {
      return twoDates[1];
    })
  };
};

/**
 * Simple check to try to guess date format, based on max value of first position.
 * If dates are consistent with US format, it will use US format (mm-dd-yyyy).
 *
 * @param  {TableColumn} tableColumn The column.
 * @return {Object} Object with keys:
 *         subtype: The identified subtype, or undefined.
 *         jsDates: The values as javascript dates.
 *         julianDates: The values as JulianDates.
 */
function convertToDates(tableColumn) {
  // Before converting to dates, we ignore values which would be replaced with null or zero.
  // Do this by replacing both sorts with null.
  var goodValues = replaceValues(
    tableColumn._values,
    [],
    tableColumn._replaceWithNullValues.concat(
      tableColumn._replaceWithZeroValues
    )
  );
  return TableColumn.convertToDates(goodValues, tableColumn.subtype);
}

// Returns true if all non-blank values could be html tags.
// Test by checking if the first character is <, and it ends with a >, has length at least 5, and it either:
//  finishes "/>" (to catch <br/>),
//  contains "=" (to catch <img src="foo">), or
//  contains another < and >, but not << or >> at the start and end (to catch <div>Foo</div>).
function looksLikeHtmlTags(values) {
  for (var i = values.length - 1; i >= 0; i--) {
    var value = values[i];
    if (value === null) {
      continue;
    }
    if (!defined(value) || !defined(value.indexOf)) {
      return false;
    }
    if (
      value[0] !== "<" ||
      value[value.length - 1] !== ">" ||
      value.length < 5
    ) {
      return false;
    }
    if (value[value.length - 2] === "/") {
      continue;
    }
    if (value.indexOf("=") >= 0) {
      continue;
    }
    var cutValue = value.substr(2, value.length - 4);
    if (cutValue.indexOf("<") > 0 && cutValue.indexOf(">") >= 0) {
      continue;
    }
    return false;
  }
  return true;
}

// zip([[1, 2, 3], [4, 5, 6]]) = [[1, 4], [2, 5], [3, 6]].
function zip(arrayOfArrays) {
  return arrayOfArrays[0].map(function(_, secondIndex) {
    return arrayOfArrays.map(function(_, firstIndex) {
      return arrayOfArrays[firstIndex][secondIndex];
    });
  });
}

/**
 * Sums the values of a number of TableColumns.
 * @param {...TableColumn} The table columns (either a single array or as separate arguments).
 * @return {Number[]} Array of values of the sum.
 */
TableColumn.sumValues = function() {
  var columns;
  if (arguments.length === 1) {
    columns = arguments[0];
  } else {
    columns = Array.prototype.slice.call(arguments); // Gives arguments a map property.
  }
  var allValues = columns.map(function(column) {
    return column.values;
  });
  var transposed = zip(allValues);
  return transposed.map(function(rowValues) {
    return rowValues.reduce(function(x, y) {
      if (x === null && y === null) {
        return null;
      }
      if (x === null) {
        return +y;
      }
      if (y === null) {
        return +x;
      }
      return +x + +y;
    });
  });
};

/**
 * Divides the values of one TableColumns into another, optionally replacing those with denominator zero.
 * @param {TableColumn} numerator The column whose values form the numerator.
 * @param {TableColumn} denominator The column whose values form the denominator.
 * @return {Number[]} Array of values of numerator / denominator.
 */
TableColumn.divideValues = function(numerator, denominator, nanReplace) {
  return denominator.values.map(function(denominatorValue, index) {
    if (denominatorValue === 0 && defined(nanReplace)) {
      return nanReplace;
    }
    return +numerator.values[index] / +denominatorValue;
  });
};

function sortMostCommonFirst(values, uniqueValues) {
  var frequencies = values.reduce(function(frequencies, thisValue) {
    if (!defined(frequencies[thisValue])) {
      frequencies[thisValue] = 1;
    } else {
      frequencies[thisValue] += 1;
    }
    return frequencies;
  }, {});
  uniqueValues.sort(function(a, b) {
    // Sort with most common value first; if two have the same frequency, sort by key order.
    return frequencies[b] - frequencies[a] || (a < b ? -1 : a > b ? 1 : 0);
  });
}

/**
 * Guesses the best variable type based on its name. Returns undefined if no guess.
 * @private
 * @param {Object[]} hintSet The hint set to use, eg. [{ hint: /^(.*[_ ])?(year)/i, type: VarSubType.YEAR }].
 * @param {String} name The variable name, eg. 'Time (AEST)'.
 * @param {VarType[]|VarSubType[]} unallowedTypes Types not to consider. Pass [] to consider all types or subtypes.
 * @return {VarType|VarSubType} The variable type or subtype, eg. VarType.SCALAR.
 */
function applyHintsToName(hintSet, name, unallowedTypes) {
  for (var i in hintSet) {
    if (hintSet[i].hint.test(name)) {
      var guess = hintSet[i].type;
      if (unallowedTypes.indexOf(guess) === -1) {
        return guess;
      }
    }
  }
}

function getFormattedValues(tableColumn) {
  if (tableColumn.type === VarType.SCALAR) {
    // Use raw values so no replacements are made in the displayed value, eg. "-" stays "-".
    return tableColumn._rawValues.map(function(value) {
      if (isNaN(value)) {
        return value;
      }
      return formatNumberForLocale(value, tableColumn.format);
    });
  } else if (tableColumn.type === VarType.TIME) {
    return tableColumn.dates.map(function(date, index) {
      // date is a javascript Date, which will display as eg. Thu Jan 28 2016 15:22:37 GMT+1100 (AEDT).
      // If the original string contains a "T", then it is ISO8601 format, and we can format it more nicely.
      var value = tableColumn._values[index];
      if (
        (typeof value === "string" || value instanceof String) &&
        value.indexOf("T") >= 0
      ) {
        // If there was no timezone info in the original, remove the timezone info from the output string.
        var time = value.split("T")[1];
        if (
          !(
            time.indexOf("+") >= 0 ||
            time.indexOf("-") >= 0 ||
            time.indexOf("Z") >= 0
          )
        ) {
          return date.toDateString() + " " + date.toTimeString().split(" ")[0];
        } else {
          return date.toString();
        }
      }
      // If it wasn't ISO8601 format with a 'T', then leave it in the original format.
      if (defined(value)) {
        return value;
      }
      return "";
    });
  } else {
    // For anything else, just replace nulls with ''
    return tableColumn._values.map(function(value) {
      return value === null ? "" : value;
    });
  }
}

/**
 * Try to determine the best variable type based on the variable name.
 * Sets the _type and _subtype properties.
 */
TableColumn.prototype.setTypeAndSubTypeFromName = function() {
  var type = applyHintsToName(typeHintSet, this.name, this._unallowedTypes);
  if (!defined(type)) {
    type = VarType.SCALAR;
    if (this._unallowedTypes.indexOf(VarType.SCALAR) >= 0) {
      throw new DeveloperError("No suitable variable type found.");
    }
  }
  this._type = type;
  this._subtype = applyHintsToName(subtypeHintSet, this.name, []);
  this._isEndDate = applyHintsToName(endDateHintSet, this.name, []);
};

/**
 * Returns this column as an array, with the name as the first element, eg. ['x', 1, 3, 4].
 * @return {Array} The column as an array.
 */
TableColumn.prototype.toArrayWithName = function() {
  return [this.name].concat(this.values);
};

/**
 * Destroy the object and release resources. Is this necessary?
 */
TableColumn.prototype.destroy = function() {
  return destroyObject(this);
};

module.exports = TableColumn;