1/**
+ 2 */
+ 3
+ 4/** */
+ 5import { Condition, filter } from './DataFilters';
+ 6
+ 7/** defines a [min-max] range */
+ 8export type NumRange = [number, number];
+ 9
+ 10/** defines a numeric domain that includes all values of a column */
+ 11export type NumDomain = [number, number];
+ 12
+ 13/** defines a Date domain that includes all values of a column */
+ 14export type DateDomain = [Date, Date];
+ 15
+ 16/** defines a categorical domain that includes all values of a column */
+ 17export type NameDomain = string[];
+ 18
+ 19/** defines a generic domain that can be any of the typed domains. */
+ 20export type Domain = NumDomain | DateDomain | NameDomain;
+ 21
+ 22/** defines a Column Reference, either as column name or index in the {@link Data.DataRow `DataRow`} array */
+ 23export type ColumnReference = number|string;
+ 24
+ 25/** a generic data value type, used in the {@link Data.DataRow `DataRow`} array */
+ 26export type DataVal = number|string|Date;
+ 27
+ 28/** a single row of column values */
+ 29export type DataRow = DataVal[];
+ 30
+ 31/** a JSON format data set, using arrays of names and rows */
+ 32export interface DataSet {
+ 33/** an optional name for the data set */
+ 34 name?: string;
+ 35/** an array of column names. Each name matches the column with the same index in DataRow */
+ 36 colNames: string[];
+ 37/** rows of data */
+ 38 rows: DataRow[];
+ 39}
+ 40
+ 41/** a JSON format data set, using an array of {name:value, ...} literals*/
+ 42export type DataLiteralSet = Array;
+ 43
+ 44interface TypeStruct { type: string; count: number;};
+ 45
+ 46interface MetaStruct {
+ 47 name: string; // column name
+ 48 column: number; // column index
+ 49 accessed: boolean; // has column data been accessed?
+ 50 cast: boolean; // has column data been cast
+ 51 types: TypeStruct[]; // data types, sorted by likelihood
+ 52}
+ 53
+ 54export type sortFn = (x:any, y:any) => number;
+ 55export type mapFn = (colVal:any, colIndex?:number, rowIndex?:number, rows?:any[][]) => any;
+ 56
+ 57/**
+ 58 * # Data
+ 59 * A simple in-memory database that holds data in rows of columns.
+ 60 *
+ 61 */
+ 62export class Data {
+ 63 //----------------------------
+ 64 // public part
+ 65 //----------------------------
+ 66 public static type = {
+ 67/** numeric values */
+ 68 number: 'number',
+ 69/** nominal values, represented by arbitrary words */
+ 70 name: 'name',
+ 71/** date values */
+ 72 date: 'date',
+ 73/** currency values. Currently support6ed are values ofg the format '$dd[,ddd]' */
+ 74 currency: 'currency',
+ 75/** percent values: 'd%' */
+ 76 percent: 'percent',
+ 77// nominal: 'nominal'
+ 78 };
+ 79
+ 80 public static toDataSet(data:DataLiteralSet, name?:string):DataSet {
+ 81 data = data || [{}];
+ 82 const names = Object.keys(data[0]);
+ 83 const rows = data.map((r:any) =>
+ 84 names.map((n:string) => r[n]));
+ 85 return { rows:rows, colNames:names, name:name||undefined };
+ 86 }
+ 87
+ 88 constructor(data?:DataSet) {
+ 89 this.import(data);
+ 90 }
+ 91
+ 92/**
+ 93 * @return the `name` field for this data base, if any
+ 94 */
+ 95 public getName():string {
+ 96 return this.name;
+ 97 }
+ 98
+ 99/**
+ 100 * Imports data from an object literal `data`
+ 101 * @param data the data set to import
+ 102 */
+ 103 public import(data:DataSet) {
+ 104 this.name = data.name;
+ 105 this.setData(data.rows, data.colNames);
+ 106 }
+ 107
+ 108/**
+ 109 * Exports to an object literal
+ 110 */
+ 111 public export():DataSet {
+ 112 return {
+ 113 rows: this.getData(),
+ 114 colNames:this.colNames()
+ 115 };
+ 116 }
+ 117
+ 118/**
+ 119 * returns the 2D array underlying the data base.
+ 120 */
+ 121 public getData():DataRow[] {
+ 122 return this.data;
+ 123 }
+ 124
+ 125/**
+ 126 * Returns the values in the specified column as a new array.
+ 127 * @param col the column to return.
+ 128 */
+ 129 public getColumn(col:ColumnReference): DataVal[] {
+ 130 const cn = this.colNumber(col);
+ 131 return this.data.map((row:DataRow) => row[cn]);
+ 132 }
+ 133
+ 134/**
+ 135 * adds a new column to the data set. if `newCol` already exists,
+ 136 * the column index is returned withoput change.
+ 137 * @param col the name of the new column
+ 138 * @return the index for the new column
+ 139 */
+ 140 public colAdd(col:string):number {
+ 141 let m = this.getMeta(col);
+ 142 if (m === undefined) {
+ 143 m = this.meta[col] = {};
+ 144 m.name = col;
+ 145 m.column = this.meta.length;
+ 146 this.meta.push(m); // access name by both column name and index
+ 147 m.cast = false; // has not been cast yet
+ 148 m.accessed = false; // has not been accessed yet
+ 149 }
+ 150 return m.column;
+ 151 }
+ 152
+ 153/**
+ 154 * initializes the specifed column with values, adding a new column if needed.
+ 155 * If `val`is a function, it is called as ```
+ 156 * val(colValue:DataVal, rowIndex:number, row:DataRow)
+ 157 * ```
+ 158 * @param col the column to initialize
+ 159 * @param initializer the value to initialize with, or a function whose return
+ 160 * value is used to initialize the column
+ 161 */
+ 162 public colInitialize(col:ColumnReference, initializer:any) {
+ 163 const cn = this.colNumber(col);
+ 164 if (!cn && typeof col === 'string') { this.colAdd(col); }
+ 165 const fn = typeof initializer === 'function';
+ 166 if (cn!==undefined) {
+ 167 this.data.map((r:DataRow, i:number) =>
+ 168 r[cn] = fn? initializer(r[cn], i, r) : initializer
+ 169 );
+ 170 }
+ 171 }
+ 172
+ 173/**
+ 174 * returns the column index of the specified column.
+ 175 * `col` can be either an index or a name.
+ 176 * @param column the data column, name or index, for which to return the index.
+ 177 * @return the column number or `undefined`.
+ 178 */
+ 179 public colNumber(col:ColumnReference) {
+ 180 const m = this.getMeta(col);
+ 181 if (!m) { return undefined; }
+ 182 else {
+ 183 m.accessed = true;
+ 184 return m.column;
+ 185 }
+ 186 }
+ 187
+ 188/**
+ 189 * returns the column name for the specified column.
+ 190 * `col` can be either an index or a name.
+ 191 * @param column the data column, name or index.
+ 192 * @return the column name or `undefined`.
+ 193 */
+ 194 public colName(col:ColumnReference) {
+ 195 var m = this.getMeta(col);
+ 196 if (!m) { return undefined; }
+ 197 m.accessed = true;
+ 198 return m.name;
+ 199 }
+ 200
+ 201/**
+ 202 * returns the names for all columns.
+ 203 * @return an array of strings with the names.
+ 204 */
+ 205 public colNames():string[] {
+ 206 return this.meta.map((m:MetaStruct) => m.name);
+ 207 }
+ 208
+ 209/**
+ 210 * returns the column type for the specified column.
+ 211 * `col` can be either an index or a name.
+ 212 * @param column the data column, name or index.
+ 213 * @return the column type.
+ 214 */
+ 215 public colType(col:ColumnReference) {
+ 216 const meta = this.getMeta(col);
+ 217 return meta? meta.types[0].type : Data.type.name;
+ 218 }
+ 219
+ 220/**
+ 221 * modifies `domain` to include all values in column `col`.
+ 222 * If no `col` is specified, the range of data indexes is returned.
+ 223 * @param col optional; the column name or index
+ 224 * @param domain optional; the Domain range to update
+ 225 * @return the updated domain
+ 226 */
+ 227 public findDomain(col?:ColumnReference, domain?:Domain):Domain {
+ 228 if (domain===undefined) { domain = []; }
+ 229 if (col === undefined) { // use array index as domain
+ 230 domain[0] = 0;
+ 231 domain[1] = this.data.length-1;
+ 232 } else {
+ 233 const c = this.colNumber(col);
+ 234 const type = this.colType(col);
+ 235 if (this.data === undefined) {
+ 236 console.log('no data');
+ 237 }
+ 238 switch(type) {
+ 239 case Data.type.name:
+ 240 this.data.forEach((r:DataRow) => {
+ 241 const nomDom = domain;
+ 242 if (nomDom.indexOf(''+r[c]) < 0) { nomDom.push(''+r[c]); }
+ 243 });
+ 244 break;
+ 245 default:
+ 246 this.data.forEach((r:DataRow) => {
+ 247 let v:number = r[c];
+ 248 if (domain[0]===undefined) { domain[0] = v; }
+ 249 if (domain[1]===undefined) { domain[1] = v; }
+ 250 if (v!==undefined && v!==null) {
+ 251 if (v
+ 252 else if (v>domain[1]) { domain[1] = v; }
+ 253 }
+ 254 });
+ 255 }
+ 256 }
+ 257 return domain;
+ 258 }
+ 259
+ 260 public castData() {
+ 261 this.meta.forEach((c:MetaStruct) => {
+ 262 const col = c.column;
+ 263 if (!c.cast) {
+ 264 this.data.forEach((row:DataRow) => row[col] = this.castVal(c.types[0].type, row[col]));
+ 265 }
+ 266 c.cast = true;
+ 267 });
+ 268 }
+ 269
+ 270/**
+ 271 * filters this data set and returns a new data set with a
+ 272 * shallow copy of rows that pass the `condition`.
+ 273 * See {@link DataFilters DataFilters} for rules and examples on how to construct conditions.
+ 274 * @param condition filters
+ 275 * @return a new Data object with rows that pass the filter
+ 276 */
+ 277 public filter(condition:Condition):Data {
+ 278 return filter(this, condition);
+ 279 }
+ 280
+ 281/**
+ 282 * @description Sorts the rows of values based on the result of the `sortFn`,
+ 283 * which behaves similarly to the Array.sort method.
+ 284 * Two modes are supported:
+ 285 * # Array Mode
+ 286 * If `col` is omitted, the column arrays are passed as samples into the `sortFn`.
+ 287 * This allows for complex sorts, combining conditions across multiple columns.
+ 288 * ```
+ 289 * data.sort((row1, row2) => row1[5] - row2[5] );
+ 290 * ```
+ 291 * # Column mode
+ 292 * If `col` is specified, either as index or by column name, the respective column value is passed
+ 293 * into `sortFn`. This allows filtering for simple conditions.
+ 294 * **The specified column will be automatically cast prior to sorting**
+ 295 * `data.sort('Date', function(val1, val2) { return val1 - val2; });`
+ 296 * @param col optional; the data column to use for sorting.
+ 297 * @param sortFn a function to implement the conditions,
+ 298 * follows the same specifications as the function passed to Array.sort().
+ 299 * Some predefined sort function can be invoked by providing a
+ 300 * respective string instead of a function. The following functions are defined:
+ 301
+
+
\ No newline at end of file
diff --git a/src/Data.spec.html b/src/Data.spec.html
new file mode 100644
index 0000000..7f18f3f
--- /dev/null
+++ b/src/Data.spec.html
@@ -0,0 +1,54 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Data.ts b/src/Data.ts
deleted file mode 100644
index 3c83a09..0000000
--- a/src/Data.ts
+++ /dev/null
@@ -1,540 +0,0 @@
-/**
- */
-
- /** */
-import { Condition, filter } from './DataFilters';
-
-/** defines a [min-max] range */
-export type NumRange = [number, number];
-
-/** defines a numeric domain that includes all values of a column */
-export type NumDomain = [number, number];
-
-/** defines a Date domain that includes all values of a column */
-export type DateDomain = [Date, Date];
-
-/** defines a categorical domain that includes all values of a column */
-export type NameDomain = string[];
-
-/** defines a generic domain that can be any of the typed domains. */
-export type Domain = NumDomain | DateDomain | NameDomain;
-
-/** defines a Column Reference, either as column name or index in the {@link Data.DataRow `DataRow`} array */
-export type ColumnReference = number|string;
-
-/** a generic data value type, used in the {@link Data.DataRow `DataRow`} array */
-export type DataVal = number|string|Date;
-
-/** a single row of column values */
-export type DataRow = DataVal[];
-
-/** a JSON format data set, using arrays of names and rows */
-export interface DataSet {
- /** an optional name for the data set */
- name?: string;
- /** an array of column names. Each name matches the column with the same index in DataRow */
- colNames: string[];
- /** rows of data */
- rows: DataRow[];
-}
-
-/** a JSON format data set, using an array of {name:value, ...} literals*/
-export type DataLiteralSet = Array;
-
-interface TypeStruct { type: string; count: number;};
-
-interface MetaStruct {
- name: string; // column name
- column: number; // column index
- accessed: boolean; // has column data been accessed?
- cast: boolean; // has column data been cast
- types: TypeStruct[]; // data types, sorted by likelihood
-}
-
-export type sortFn = (x:any, y:any) => number;
-export type mapFn = (colVal:any, colIndex?:number, rowIndex?:number, rows?:any[][]) => any;
-
-/**
- * # Data
- * A simple in-memory database that holds data in rows of columns.
- *
- */
-export class Data {
- //----------------------------
- // public part
- //----------------------------
- public static type = {
- number: 'number data',
- name: 'name data',
- date: 'date data',
- currency: 'currency data',
- percent: 'percent data',
- nominal: 'nominal data'
- };
-
- public static toDataSet(data:DataLiteralSet, name?:string):DataSet {
- data = data || [{}];
- const names = Object.keys(data[0]);
- const rows = data.map((r:any) =>
- names.map((n:string) => r[n]));
- return { rows:rows, colNames:names, name:name||undefined };
- }
-
- constructor(data?:DataSet) {
- this.import(data);
- }
-
- /**
- * @return the `name` field for this data base, if any
- */
- public getName():string {
- return this.name;
- }
-
- /**
- * Imports data from an object literal `data`
- * @param data the data set to import
- */
- public import(data:DataSet) {
- this.name = data.name;
- this.setData(data.rows, data.colNames);
- }
-
- /**
- * Exports to an object literal
- */
- public export():DataSet {
- return {
- rows: this.getData(),
- colNames:this.colNames()
- };
- }
-
- /**
- * returns the 2D array underlying the data base.
- */
- public getData():DataRow[] {
- return this.data;
- }
-
- /**
- * Returns the values in the specified column as a new array.
- * @param col the column to return.
- */
- public getColumn(col:ColumnReference): DataVal[] {
- const cn = this.colNumber(col);
- return this.data.map((row:DataRow) => row[cn]);
- }
-
- /**
- * adds a new column to the data set. if `newCol` already exists,
- * the column index is returned withoput change.
- * @param col the name of the new column
- * @return the index for the new column
- */
- public colAdd(col:string):number {
- let m = this.getMeta(col);
- if (m === undefined) {
- m = this.meta[col] = {};
- m.name = col;
- m.column = this.meta.length;
- this.meta.push(m); // access name by both column name and index
- m.cast = false; // has not been cast yet
- m.accessed = false; // has not been accessed yet
- }
- return m.column;
- }
-
- /**
- * initializes the specifed column with values, adding a new column if needed.
- * If `val`is a function, it is called as ```
- * val(colValue:DataVal, rowIndex:number, row:DataRow)
- * ```
- * @param col the column to initialize
- * @param initializer the value to initialize with, or a function whose return
- * value is used to initialize the column
- */
- public colInitialize(col:ColumnReference, initializer:any) {
- const cn = this.colNumber(col);
- if (!cn && typeof col === 'string') { this.colAdd(col); }
- const fn = typeof initializer === 'function';
- if (cn!==undefined) {
- this.data.map((r:DataRow, i:number) =>
- r[cn] = fn? initializer(r[cn], i, r) : initializer
- );
- }
- }
-
- /**
- * returns the column index of the specified column.
- * `col` can be either an index or a name.
- * @param column the data column, name or index, for which to return the index.
- * @return the column number or `undefined`.
- */
- public colNumber(col:ColumnReference) {
- const m = this.getMeta(col);
- if (!m) { return undefined; }
- else {
- m.accessed = true;
- return m.column;
- }
- }
-
- /**
- * returns the column name for the specified column.
- * `col` can be either an index or a name.
- * @param column the data column, name or index.
- * @return the column name or `undefined`.
- */
- public colName(col:ColumnReference) {
- var m = this.getMeta(col);
- if (!m) { return undefined; }
- m.accessed = true;
- return m.name;
- }
-
- /**
- * returns the names for all columns.
- * @return an array of strings with the names.
- */
- public colNames():string[] {
- return this.meta.map((m:MetaStruct) => m.name);
- }
-
- /**
- * returns the column type for the specified column.
- * `col` can be either an index or a name.
- * @param column the data column, name or index.
- * @return the column type.
- */
- public colType(col:ColumnReference) {
- const meta = this.getMeta(col);
- return meta? meta.types[0].type : Data.type.name;
- }
-
- /**
- * modifies `domain` to include all values in column `col`.
- * @param col the column name or index
- * @param domain the
- */
- public findDomain(col:ColumnReference, domain:Domain) {
- if (col === undefined) { // use array index as domain
- domain[0] = 0;
- domain[1] = this.data.length-1;
- } else {
- const c = this.colNumber(col);
- const type = this.colType(col);
- if (this.data === undefined) {
- console.log('no data');
- }
- switch(type) {
- case Data.type.nominal:
- this.data.forEach((r:DataRow) => {
- const nomDom = domain;
- if (nomDom.indexOf(''+r[c]) < 0) { nomDom.push(''+r[c]); }
- });
- break;
- default:
- this.data.forEach((r:DataRow) => {
- let v:number = r[c];
- if (v!==undefined && v!==null) {
- domain[0] = (vdomain[1])? v : domain[1];
- }
- });
- }
- }
- }
-
- public castData() {
- this.meta.forEach((c:MetaStruct) => {
- const col = c.column;
- if (!c.cast) {
- this.data.forEach((row:DataRow) => row[col] = this.castVal(c.types[0].type, row[col]));
- }
- c.cast = true;
- });
- }
-
- /**
- * filters this data set and returns a new data set with a
- * shallow copy of rows that pass the `condition`.
- * See {@link DataFilters DataFilters} for rules and examples on how to construct conditions.
- * @param condition filters
- * @return a new Data object with rows that pass the filter
- */
- public filter(condition:Condition):Data {
- return filter(this, condition);
- }
-
- /**
- * @description Sorts the rows of values based on the result of the `sortFn`,
- * which behaves similarly to the Array.sort method.
- * Two modes are supported:
- * # Array Mode
- * If `col` is omitted, the column arrays are passed as samples into the `sortFn`.
- * This allows for complex sorts, combining conditions across multiple columns.
- * ```
- * data.sort((row1, row2) => row1[5] - row2[5] );
- * ```
- * # Column mode
- * If `col` is specified, either as index or by column name, the respective column value is passed
- * into `sortFn`. This allows filtering for simple conditions.
- * **The specified column will be automatically cast prior to sorting**
- * `data.sort('Date', function(val1, val2) { return val1 - val2; });`
- * @param col optional; the data column to use for sorting.
- * @param sortFn a function to implement the conditions,
- * follows the same specifications as the function passed to Array.sort().
- * Some predefined sort function can be invoked by providing a
- * respective string instead of a function. The following functions are defined:
-
replace value with itself, performing no operation.
-
'cumulate'
replace value with the cumulative sum of values up to the current element.
-
- * @return a new Data object containing the mapping.
- */
- public map(col:ColumnReference|ColumnReference[], mapFn:string|mapFn):Data {
- const noop = (val:any) => val;
- const cumulate = () => {
- let sum=0;
- return (val:number, i:number) => { sum += +val; return sum; };
- };
- function getFn() {
- let fn; // define fn inside each col loop to ensure initialization
- switch (mapFn) {
- case 'cumulate': fn = cumulate(); break;
- case 'noop': fn = noop; break;
- default: fn = mapFn;
- }
- return fn;
- }
-
- let result = new Data({colNames:this.colNames(), rows:this.data.slice(), name:this.getName()});
-
- const names = col['length']? col : [col];
- names.map((cn:ColumnReference) => {
- const c = this.colNumber(cn);
- let fn = getFn(); // define fn inside each col loop to ensure initialization
- result.data = result.data.map((row:any[], i:number, rows:any[][]) => {
- row[c] = fn(row[c], c, i, rows);
- return row;
- });
- });
- return result;
- }
-
- //----------------------------
- // private part
- //----------------------------
- private data: DataRow[] = [];
- private meta: MetaStruct[] = [];
- private name: string;
-
- private getMeta(col:ColumnReference):MetaStruct {
- if (!this.meta) { this.meta = []; }
- if (!this.meta[col]) { return undefined; }
- this.meta[col].accessed = true;
- return this.meta[col];
- }
-
- /**
- * sets `data` to the existing data set. If data has previously been set,
- * `data` will be added to the end of the list if all `names` match those of the
- * existing set.
- * @param data the data to add
- * @param names an array of names that match the columns
- * @param autoType unless set to false, the method will attempt to determine the
- * type of data and automatically cast data points to their correct value
- */
- private setData(data:DataRow[], names:string[], autoType=true):void {
- this.meta = [];
- this.data = data;
- if (!names) {
- console.log();
- }
- names.forEach((col:string) => this.colAdd(col));
- names.forEach((col:string) => this.findTypes(col));
- this.castData();
- }
-
- /**
- * Determines the type of data in `col`. An array of counts is created for all
- * encountered types, sorted by descending frequency. THe most likely type in position 0
- * of the array is returned.
- * @param col the index of the column to be typed.
- * @return the most likely type of data in `col`.
- */
- private findTypes(col:ColumnReference):string {
- const m = this.getMeta(col);
- const types:TypeStruct[] = [];
- Object.keys(Data.type).forEach((t:string) => {
- const ts = { type: Data.type[t], count: 0 };
- types.push(ts);
- types[Data.type[t]] = ts;
- });
- for (let v of this.allRows(col)) {
- const t = this.findType(v);
- if (t !== null) { types[t].count++; }
- }
- types.sort(function(a:TypeStruct, b:TypeStruct) {
- if (a.type==='currency'&&a.count>0) { return -1; }
- if (b.type==='currency'&&b.count>0) { return 1; }
- return b.count - a.count;
- });
- m.types = types;
- return types[0].type;
- }
-
- /**
- * @description determines the data type. Supported types are
- * ```
- * 'date': sample represents a Date, either as a Date object or a String
- * 'number': sample represents a number
- * 'percent': sample represents a percentage (special case of a real number)
- * 'nominal': sample represents a nominal (ordinal or categorical) value
- * ```
- * @param val the value to bve typed.
- * @returns the type ('number', 'date', 'percent', 'nominal', 'currency') corresponding to the sample
- */
- private findType(val:DataVal) {
- if (val && val!=='') {
- if (val instanceof Date) { return Data.type.date; } // if val is already a date
- if (typeof val === 'number') { return Data.type.number; } // if val is already a number
-
- // else: val is a string:
- const strVal = ''+val;
- if (''+parseFloat(strVal) === strVal) { return Data.type.number; }
- if (strVal.startsWith('$') && !isNaN(parseFloat(strVal.slice(1)))) { return Data.type.currency; }
- if (strVal.endsWith('%') && !isNaN(parseFloat(strVal))) { return Data.type.percent; }
- if (!isNaN(this.toDate(strVal).getTime())) { return Data.type.date; }
-
- // european large number currency representation: '$dd,ddd[,ddd]'
- if ((/^\$\d{0,2}((,\d\d\d)*)/g).test(val)) {
- if (!isNaN(parseFloat(val.trim().replace(/[^eE\+\-\.\d]/g, '').replace(/,/g, '')))) {
- return Data.type.currency;
- }
- }
- switch (strVal.toLowerCase()) {
- case "null": break;
- case "#ref!": break;
- default: if (val.length>0) { return Data.type.nominal; }
- }
- }
- return null;
- }
-
- /**
- * A generator that provides the specified column value for each row in `Data` in sequence.
- * @param column
- */
- private * allRows(column:ColumnReference):Iterable {
- const c = this.colNumber(column);
- for (let r=0; rval; }
- else { d = new Date(val); }
- let yr=d.getFullYear();
- if (yr < 100) {
- yr += 1900;
- d.setFullYear( (yr < limitYear)? yr+100 : yr);
- }
- return d;
- }
-
- /**
- * @param type ['date' | 'percent' | 'real' | _any_] The type to cast into. In case of _any_ - i.e. `type`
- * does not match any of the previous keywords, no casting occurs.
- * @param sample The value to cast.
- * @returns The result of the cast.
- * @description Casts the sample to the specified data type.
- */
- private castVal(type:string, val:DataVal):DataVal {
- switch (type) {
- case Data.type.date: if (val instanceof Date) { return val; }
- val = this.toDate(val);
- if (isNaN(val.getTime())) { val = null; }
- break;
- case Data.type.percent: if (typeof val === 'string') {
- const num = parseFloat(val);
- val = (val).endsWith('%')? num/100 : num;
- }
- if (isNaN(val)) { val = null; }
- break;
- case Data.type.currency:// replace all except 'e/E', '.', '+/-' and digits
- if (typeof val === 'string') { val = val.replace(/[^eE\+\-\.\d]/g, ''); }
- /* falls through */
- case Data.type.number: if (typeof val === 'string') { val = parseFloat(val); }
- if (isNaN(val)) { val = null; }
- break;
- default: val = ''+val;
- }
- return val;
- }
-}
\ No newline at end of file
diff --git a/src/DataFilters.html b/src/DataFilters.html
new file mode 100644
index 0000000..7669d54
--- /dev/null
+++ b/src/DataFilters.html
@@ -0,0 +1,267 @@
+
+
+
src/DataFilters.ts
+
1
+ 2/**
+ 3* Use the {@link filter `filter`} function to executes a queries on a {@link Data `Data`} object.
+ 4* Each row in the data is checked and those for which `conditions` holds true are returned as a new `Data` object.
+ 5*
+ 6* # Condition construction
+ 7*
+ 8* ### General Condition
+ 9* ```
+ 10* Condition =
+ 11* IndexCondition -> conditions on the row index
+ 12* || RecursiveCondition -> (set of) conditions on column values
+ 13* ```
+ 14*
+ 15* ### IndexCondition
+ 16* ```
+ 17* IndexCondition =
+ 18* rowIndex:number -> true if row index matches
+ 19* ```
+ 20*
+ 21* ### RecursiveCondition
+ 22* ```
+ 23* RecursiveCondition =
+ 24* OrCondition -> OR: true if any compound condition is true
+ 25* || AndCondition -> AND: true if all compound conditions are true
+ 26*
+ 27* OrCondition = -> OR: true if
+ 28* AndCondition[] -> any of the AndConditions are true
+ 29* || IndexCondition[] -> any of thr IndexConditions are true
+ 30*
+ 31* AndCondition = -> AND: true if
+ 32* SetAndCondition -> all SetAndConditions are true
+ 33* || TermAndCondition -> or if all TermAndConditions are true
+ 34*
+ 35* SetAndCondition = { -> AND: true if all sub-conditions are true
+ 36* 'or': RecursiveCondition -> true if any RecursiveCondition is true
+ 37* || 'and': RecursiveCondition -> true if all RecursiveCondition are true
+ 38* || 'not': RecursiveCondition -> true if the condition is false
+ 39*
+ 40* TermAndCondition = { -> Terminal AND: true if all terminal sub-conditions are true
+ 41* colDesc:colValue -> true if colValue matches
+ 42* || colDesc:[colValue, ...] -> true if any of the colValues match
+ 43* || colDesc:function(value,row) -> true if function returns true
+ 44* }
+ 45*
+ 46* colDesc = either column name or index
+ 47* ```
+ 48*
+ 49* ### Practical Tips
+ 50* ```
+ 51* {'or': [recurCond, ...]} -> OR, same as [recurCond, ...]
+ 52* || {'or': {SetCond, ...}} -> OR, same as [SetCond, ...]
+ 53* || {'and': [recurCond, ...]} -> AND, true if all recurCond are true
+ 54* || {'and': {SetCond, ...}} -> AND, same as {SetCond, ...}
+ 55* || {'not': {SetCond, ...}} -> NAND: true if the SetCond are true
+ 56* || {'not': [recurCond, ...]} -> NOR: true if any of the recurCond are true
+ 57* ```
+ 58*
+ 59* # Example
+ 60*
+ 61*
+ 62* const colNames = ['Name', 'Value', 'Start', 'End'];
+ 63* const rows = [
+ 64* ['Harry', '100', '3/1/14', '11/20/14'],
+ 65* ['Mary', '1500', '7/1/14', '9/30/14'],
+ 66* ['Peter', '400', '5/20/14', '4/30/15'],
+ 67* ['Jane', '700', '11/13/14', '8/15/15']
+ 68* ]
+ 69* const data = new hsdatab.Data({colNames:colNames, rows:rows});
+ 70*
+ 71* queries = [
+ 72* ['0', undefined, 'undefined query => pass all'],
+ 73* ['1', [], 'empty OR: [] => fail all'],
+ 74* ['2', {}, 'empty AND: {} => pass all'],
+ 75* ['3', 1, '2nd row: pass row 1'],
+ 76* ['4', [1,3], '2nd+4th: pass rows: 1 and 3'],
+ 77* ['5', {Name:"Jane"}, 'Name is Jane'],
+ 78* ['6', {1:1500}, 'Column 2 is 1500'],
+ 79* ['7', {Name:["Peter", "Jane"]}, 'Name is Peter or Jane'],
+ 80* ['8', [{Name:"Peter"}, {Value:1500}], 'Name is Peter or Value is 1500'],
+ 81* ['9', {or:{Name:"Peter", Value:1500}}, 'OR: same as 8:'],
+ 82* ['A', {or:[{Name:"Peter"}, {Value:1500}]}, 'OR: [{Name is Peter}, {Value is 1500}]'],
+ 83* ['B', {Name:"Peter", Value:400}, 'Name is Peter and Value is 400'],
+ 84* ['C', {and:{Name:"Peter", Value:400}}, 'AND: {Name is Peter, Value is 400}'],
+ 85* ['D', {and:{Name:"Peter", Value:1500}}, 'AND: {Name is Peter, Value is 1500}'],
+ 86* ['E', {and:[{Name:"Peter"}, {Value:400}]}, 'AND:[{Name is Peter}, {Value is 400}]'],
+ 87* ['F', {and:[{Name:"Peter"}, {Value:1500}]},'AND:[{Name is Peter}, {Value is 1500}]'],
+ 88* ['G', {not:{Name:"Peter", Value:400}}, 'NAND: not {Name is Peter and Value is 400}'],
+ 89* ['H', {not:{Name:"Peter", Value:1500}}, 'NAND: not {Name is Peter and Value is 1500}'],
+ 90* ['I', {not:[{Name:"Peter"}, {Value:1500}]},'NOR: not [{Name is Peter} or {Value is 1500}]'],
+ 91* ['J', {Name:(v) => v.length===4}, 'Name has 4 letters']
+ 92* ];
+ 93*
+ 94* m.mount(root, {
+ 95* view:() => m('', [
+ 96* m('h3', 'Given the data set:'),
+ 97* m('table#data', [
+ 98* m('tr', colNames.map(n => m('th', n))),
+ 99* ...rows.map(row => m('tr', [m('td', row[0]),m('td', row[1]),m('td', row[2].toDateString()),m('td', row[3].toDateString())]))
+ 100* ]),
+ 101* m('h3', 'The following queries yield:'),
+ 102* m('table', [
+ 103* m('tr', [m('th','#'), m('th','Query'), m('th',"Live Result, by 'Name' field")]),
+ 104* ...queries.map(q => {
+ 105* const result = data.filter(q[1]).getColumn('Name').join(', ');
+ 106* return m('tr', [m('td',`${q[0]}:`), m('td',`${q[2]}`), m('td',`[ ${result} ]`)]);
+ 107* })
+ 108* ])
+ 109* ])
+ 110* });
+ 111*
+ 112*
+ 113* $exampleID { height: 600px; }
+ 114* #data th { width:15%; }
+ 115* table {
+ 116* font-size: 10pt;
+ 117* margin-left: 10px;
+ 118* }
+ 119*
+ 120*
+ 121*/
+ 122
+ 123/** */
+ 124import { Data,
+ 125 DataVal,
+ 126 DataRow
+ 127} from './Data';
+ 128
+ 129export type Condition = IndexCondition | RecursiveCondition;
+ 130
+ 131/** true if row index matches the number(s) */
+ 132export type IndexCondition = number;
+ 133
+ 134export type RecursiveCondition = AndCondition | OrCondition;
+ 135export type OrCondition = AndCondition[] | IndexCondition[];
+ 136export type AndCondition = SetAndCondition | TermAndCondition;
+ 137
+ 138export interface SetAndCondition {
+ 139 or?: RecursiveCondition;
+ 140 and?:RecursiveCondition;
+ 141 not?:RecursiveCondition;
+ 142};
+ 143
+ 144export interface TermAndCondition {
+ 145 [colDesc:string]:
+ 146 DataVal
+ 147 | DataVal[]
+ 148 | TermConditionFunction
+ 149 ;
+ 150};
+ 151
+ 152export type TermConditionFunction = (value:DataVal, row:DataRow) => boolean;
+ 153
+ 154
+ 155function resolveTerminalCondition(name:string, val:any, row:DataRow, colNumber:(name:string)=>number):boolean {
+ 156 const col = colNumber(name);
+ 157 const valIsFunction = (typeof val === 'function');
+ 158 const valIsArray = ((typeof val === 'object') && (val.length!==undefined));
+ 159 if (isNaN(col)) {
+ 160 console.log(`column name '${name}' cannot be resolved in terminal condition ${name}=${val}`);
+ 161 console.log(row);
+ 162 return false; // -> this condition is not met;
+ 163 } else if (valIsFunction) {
+ 164 // query true if function evaluates to true
+ 165 return val(row[col], row);
+ 166 } else if (valIsArray) {
+ 167 // query true if empty array, or at least one c true
+ 168 return (val.length === 0) || val.some((v:any) => row[col] === v);
+ 169 } else { // object: all conditions have to be met, unless specified as or
+ 170 return (row[col] === val);
+ 171 }
+ 172}
+ 173
+ 174/**
+ 175 * applies `condition` to a row of data and returns `true` if the row passes.
+ 176 * @param condition the complex condition to test against
+ 177 * @param r the row index in the data set
+ 178 * @param row the row values
+ 179 * @param and
+ 180 */
+ 181function resolveCondition(condition:Condition, row:DataRow, r:number, colNumber:(name:string)=>number, and=true):boolean {
+ 182 let orResult = false;
+ 183 let andResult= true;
+ 184 // undefined condition is TRUE
+ 185 if (condition===undefined) { return true; }
+ 186
+ 187 // Simple Index Condition on row index:
+ 188 else if (typeof condition === 'number') { return (condition === r); }
+ 189
+ 190 // Recursive Condition - OR: [...], AND {...}:
+ 191 else if (typeof condition === 'object') {
+ 192 // array -> or condition on a list of row indices or compound conditions
+ 193 const mc = condition;
+ 194
+ 195 // OR condition: [...]
+ 196 if (mc.length !== undefined) {
+ 197 return (mc.length === 0)?
+ 198 // empty OR is false:
+ 199 false :
+ 200 // else: OR is true if any sub-condition is met
+ 201 mc.some((cd:AndCondition) => resolveCondition(cd, row, r, colNumber, false));
+ 202 }
+ 203 // AND condition: {...}
+ 204 else {
+ 205 for (const name in condition) {
+ 206 let conditionMet = and; // initialize with false for OR, and true for AND conjunction
+ 207 const setCond = condition;
+ 208
+ 209 // resolve SetConditions:
+ 210 switch (name) {
+ 211 case 'or': conditionMet = resolveCondition(setCond.or, row, r, colNumber, false); break;
+ 212 case 'and': conditionMet = resolveCondition(setCond.and, row, r, colNumber, true); break;
+ 213 case 'not': conditionMet = !resolveCondition(setCond.not, row, r, colNumber, true); break;
+ 214 default: conditionMet = resolveTerminalCondition(name, condition[name], row, colNumber);
+ 215 }
+ 216 if (conditionMet) { orResult = true; if(!and) { break; }} // OR conjunction: exit for name loop if condition met
+ 217 else { andResult = false; if(and) { break; }} // AND conjunction: exit for name loop if condition not met
+ 218 }
+ 219 }
+ 220 } else {
+ 221 console.error(`unrecognized condition: ${JSON.stringify(condition)}`);
+ 222 return false;
+ 223 }
+ 224 return and? andResult : orResult;
+ 225}
+ 226
+ 227/**
+ 228 * filters a `Data` object for the given `Condition`s and returns a new `Data` object with those rows for which
+ 229 * `cond` holds true.
+ 230 * @param data the `Data` object to filter
+ 231 * @param cond the complex condition to test against
+ 232 * @return a new `Data` object with the filtered rows
+ 233 */
+ 234export function filter(data:Data, cond:Condition):Data {
+ 235 const colNumber = (name:string):number => data.colNumber(name);
+ 236 try {
+ 237 return new Data({
+ 238 name: data.getName(),
+ 239 colNames: data.colNames(),
+ 240 rows:data.getData().filter((row:DataRow, i:number) => {
+ 241 const keep = resolveCondition(cond, row, i, colNumber);
+ 242 return keep;
+ 243 })
+ 244 });
+ 245 } catch(err) {
+ 246 console.log(err);
+ 247 console.log(err.stack);
+ 248 }
+ 249}
+ 250
+
+
\ No newline at end of file
diff --git a/src/DataFilters.ts b/src/DataFilters.ts
deleted file mode 100644
index 18a2261..0000000
--- a/src/DataFilters.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-
-/**
-*
-* The HsData object will feature its own column meta information, as well as
-* a copy of the rows array which allows for `filter` and `sort` operations.
-* However, the column arrays will be shared with the original HsData object in order to be memory efficient.
-* This means that `map` and `newColumn` operations on the new object will affect the original object or any
-* object derived via `query`.
-* @description executes a query on the data. Each row in the data is checked and those for which
-* `conditions` is true are returned as a new HsData object.
-*
-* ## General Condition
-* ```
-* Condition =
-* IndexCondition -> conditions on the row index
-* || RecursiveCondition -> (set of) conditions on column values
-* ```
-*
-* ## IndexCondition
-* ```
-* IndexCondition =
-* rowIndex:number -> true if row index matches
-* ```
-*
-* ## RecursiveCondition
-* ```
-* RecursiveCondition =
-* OrCondition -> OR: true if any compound condition is true
-* || AndCondition -> AND: true if all compound conditions are true
-*
-* OrCondition = -> OR: true if
-* AndCondition[] -> any of the AndConditions are true
-* || IndexCondition[] -> any of thr IndexConditions are true
-*
-* AndCondition = -> AND: true if
-* SetAndCondition -> all SetAndConditions are true
-* || TermAndCondition -> or if all TermAndConditions are true
-*
-* SetAndCondition = { -> AND: true if all sub-conditions are true
-* 'or': RecursiveCondition -> true if any RecursiveCondition is true
-* || 'and': RecursiveCondition -> true if all RecursiveCondition are true
-* || 'not': RecursiveCondition -> true if the condition is false
-*
-* TermAndCondition = { -> Terminal AND: true if all terminal sub-conditions are true
-* colDesc:colValue -> true if colValue matches
-* || colDesc:[colValue, ...] -> true if any of the colValues match
-* || colDesc:function(value,row) -> true if function returns true
-* }
-*
-* colDesc = either column name or index
-* ```
-*
-* ## Practical Tips
-* ```
-* {'or': [recurCond, ...]} -> OR, same as [recurCond, ...]
-* || {'or': {SetCond, ...}} -> OR, same as [SetCond, ...]
-* || {'and': [recurCond, ...]} -> AND, true if all recurCond are true
-* || {'and': {SetCond, ...}} -> AND, same as {SetCond, ...}
-* || {'not': {SetCond, ...}} -> NAND: true if the SetCond are true
-* || {'not': [recurCond, ...]} -> NOR: true if any of the recurCond are true
-* ```
-*
-* # Example
-*
-*
-* const colNames = ['Name', 'Value', 'Start', 'End'];
-* const rows = [
-* ['Harry', '100', '3/1/14', '11/20/14'],
-* ['Mary', '1500', '7/1/14', '9/30/14'],
-* ['Peter', '400', '5/20/14', '4/30/15'],
-* ['Jane', '700', '11/13/14', '8/15/15']
-* ]
-* const data = new hsdata.Data({colNames:colNames, rows:rows});
-*
-* queries = [
-* ['0', undefined, 'undefined query => pass all'],
-* ['1', [], 'empty OR: [] => fail all'],
-* ['2', {}, 'empty AND: {} => pass all'],
-* ['3', 1, '2nd row: pass row 1'],
-* ['4', [1,3], '2nd+4th: pass rows: 1 and 3'],
-* ['5', {Name:"Jane"}, 'Name is Jane'],
-* ['6', {1:1500}, 'Column 2 is 1500'],
-* ['7', {Name:["Peter", "Jane"]}, 'Name is Peter or Jane'],
-* ['8', [{Name:"Peter"}, {Value:1500}], 'Name is Peter or Value is 1500'],
-* ['9', {or:{Name:"Peter", Value:1500}}, 'OR: same as 8:'],
-* ['A', {or:[{Name:"Peter"}, {Value:1500}]}, 'OR: [{Name is Peter}, {Value is 1500}]'],
-* ['B', {Name:"Peter", Value:400}, 'Name is Peter and Value is 400'],
-* ['C', {and:{Name:"Peter", Value:400}}, 'AND: {Name is Peter, Value is 400}'],
-* ['D', {and:{Name:"Peter", Value:1500}}, 'AND: {Name is Peter, Value is 1500}'],
-* ['E', {and:[{Name:"Peter"}, {Value:400}]}, 'AND:[{Name is Peter}, {Value is 400}]'],
-* ['F', {and:[{Name:"Peter"}, {Value:1500}]},'AND:[{Name is Peter}, {Value is 1500}]'],
-* ['G', {not:{Name:"Peter", Value:400}}, 'NAND: not {Name is Peter and Value is 400}'],
-* ['H', {not:{Name:"Peter", Value:1500}}, 'NAND: not {Name is Peter and Value is 1500}'],
-* ['I', {not:[{Name:"Peter"}, {Value:1500}]},'NOR: not [{Name is Peter} or {Value is 1500}]'],
-* ['J', {Name:(v) => v.length===4}, 'Name has 4 letters']
-* ];
-*
-* m.mount(root, {
-* view:() => m('', [
-* m('h3', 'Given the data set:'),
-* m('table#data', [
-* m('tr', colNames.map(n => m('th', n))),
-* ...rows.map(row => m('tr', [m('td', row[0]),m('td', row[1]),m('td', row[2].toDateString()),m('td', row[3].toDateString())]))
-* ]),
-* m('h3', 'The following queries yield:'),
-* m('table', [
-* m('tr', [m('th','#'), m('th','Query'), m('th',"Live Result, by 'Name' field")]),
-* ...queries.map(q => {
-* const result = data.filter(q[1]).getColumn('Name').join(', ');
-* return m('tr', [m('td',`${q[0]}:`), m('td',`${q[2]}`), m('td',`[ ${result} ]`)]);
-* })
-* ])
-* ])
-* });
-*
-*
-* $exampleID { height: 600px; }
-* #data th { width:15%; }
-* table {
-* font-size: 10pt;
-* margin-left: 10px;
-* }
-*
-*
-*/
-
-/** */
-import { Data,
- DataVal,
- DataRow
-} from './Data';
-
-export type Condition = IndexCondition | RecursiveCondition;
-
-/** true if row index matches the number(s) */
-export type IndexCondition = number;
-
-export type RecursiveCondition = AndCondition | OrCondition;
-export type OrCondition = AndCondition[] | IndexCondition[];
-export type AndCondition = SetAndCondition | TermAndCondition;
-
-export interface SetAndCondition {
- or?: RecursiveCondition;
- and?:RecursiveCondition;
- not?:RecursiveCondition;
-};
-
-export interface TermAndCondition {
- [colDesc:string]:
- DataVal
- | DataVal[]
- | TermConditionFunction
- ;
-};
-
-export type TermConditionFunction = (value:DataVal, row:DataRow) => boolean;
-
-
-function resolveTerminalCondition(name:string, val:any, row:DataRow, colNumber:(name:string)=>number):boolean {
- const col = colNumber(name);
- const valIsFunction = (typeof val === 'function');
- const valIsArray = ((typeof val === 'object') && (val.length!==undefined));
- if (isNaN(col)) {
- console.log(`column name '${name}' cannot be resolved in terminal condition ${name}=${val}`);
- console.log(row);
- return false; // -> this condition is not met;
- } else if (valIsFunction) {
- // query true if function evaluates to true
- return val(row[col], row);
- } else if (valIsArray) {
- // query true if empty array, or at least one c true
- return (val.length === 0) || val.some((v:any) => row[col] === v);
- } else { // object: all conditions have to be met, unless specified as or
- return (row[col] === val);
- }
-}
-
-/**
- * applies `condition` to a row of data and returns `true` if the row passes.
- * @param condition the complex condition to test against
- * @param r the row index in the data set
- * @param row the row values
- * @param and
- */
-function resolveCondition(condition:Condition, row:DataRow, r:number, colNumber:(name:string)=>number, and=true):boolean {
- let orResult = false;
- let andResult= true;
- // undefined condition is TRUE
- if (condition===undefined) { return true; }
-
- // Simple Index Condition on row index:
- else if (typeof condition === 'number') { return (condition === r); }
-
- // Recursive Condition - OR: [...], AND {...}:
- else if (typeof condition === 'object') {
- // array -> or condition on a list of row indices or compound conditions
- const mc = condition;
-
- // OR condition: [...]
- if (mc.length !== undefined) {
- return (mc.length === 0)?
- // empty OR is false:
- false :
- // else: OR is true if any sub-condition is met
- mc.some((cd:AndCondition) => resolveCondition(cd, row, r, colNumber, false));
- }
- // AND condition: {...}
- else {
- for (const name in condition) {
- let conditionMet = and; // initialize with false for OR, and true for AND conjunction
- const setCond = condition;
-
- // resolve SetConditions:
- switch (name) {
- case 'or': conditionMet = resolveCondition(setCond.or, row, r, colNumber, false); break;
- case 'and': conditionMet = resolveCondition(setCond.and, row, r, colNumber, true); break;
- case 'not': conditionMet = !resolveCondition(setCond.not, row, r, colNumber, true); break;
- default: conditionMet = resolveTerminalCondition(name, condition[name], row, colNumber);
- }
- if (conditionMet) { orResult = true; if(!and) { break; }} // OR conjunction: exit for name loop if condition met
- else { andResult = false; if(and) { break; }} // AND conjunction: exit for name loop if condition not met
- }
- }
- } else {
- console.error(`unrecognized condition: ${JSON.stringify(condition)}`);
- return false;
- }
- return and? andResult : orResult;
-}
-
-export type ReduceFn = (keep:boolean, row:DataRow, i:number) => void;
-
-export function filter(data:Data, cond:Condition, reduceFn?:string|ReduceFn):Data {
- const noop = () => 0;
- const colNumber = (name:string):number => data.colNumber(name);
- let fn:ReduceFn;
- switch (reduceFn) {
- default: fn = (typeof reduceFn === 'function')? reduceFn : noop;
- }
- try {
- return new Data({
- name: data.getName(),
- colNames: data.colNames(),
- rows:data.getData().filter((row:DataRow, i:number) => {
- const keep = resolveCondition(cond, row, i, colNumber);
- if (fn) { fn(keep, row, i); }
- return keep;
- })
- });
- } catch(err) {
- console.log(err);
- console.log(err.stack);
- }
-}
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..07a3840
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,32 @@
+
+
+