jupyter-ijavascript-utils 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DOCS.md CHANGED
@@ -46,6 +46,7 @@ This is not intended to be the only way to accomplish many of these tasks, and a
46
46
 
47
47
  ## What's New
48
48
 
49
+ * 1.19 - add in {@link module:describe|describe} and {@link module:hashMap|hashMap} modules, along with {@link module:format.limitLines|format.limitLines}
49
50
  * 1.18 - tie to vega-datasets avoiding esmodules until ijavascript can support them
50
51
  * 1.17 - provide object.propertyValueSample - as a way to list 'non-empty' property values
51
52
  * 1.16 - provide file.matchFiles - as a way to find files or directories
@@ -73,9 +74,11 @@ This is not intended to be the only way to accomplish many of these tasks, and a
73
74
  | {@link module:array} | Massage, sort, reshape arrays. |
74
75
  | {@link module:base64} | Convert to and from base64 encoding of strings |
75
76
  | {@link module:datasets} | Load example <a href="https://github.com/vega/vega-datasets">datasets provided by the vega team</a> |
77
+ | {@link module:describe} | Similar to Pandas describe, provides statistics on a set of values / objects |
76
78
  | {@link module:file} | Read and write data/text to files. |
77
79
  | {@link module:format} | Formatting and massage data to be legible. |
78
80
  | {@link module:group} | Group/Reduce Hierarchies of Object - generating Maps of records ({@link SourceMap}) |
81
+ | {@link module:hashMap} | Modify JavaScript HashMaps (ex new Map()) |
79
82
  | {@link module:ijs} | Extend iJavaScript to support await, and new types of rendering - like {@tutorial htmlScript} and markdown|
80
83
  | {@link module:latex} | Render Math Notation with <a href="www.latex-project.org">LaTeX<a> and <a href="katex.org">KaTeX</a>|
81
84
  | {@link module:leaflet} | Render maps with <a href="leaflet.org">Leaflet</a> |
package/README.md CHANGED
@@ -46,6 +46,7 @@ This is not intended to be the only way to accomplish many of these tasks, and a
46
46
 
47
47
  # What's New
48
48
 
49
+ * 1.19 - add in describe and hashMap modules, along with format.limitLines
49
50
  * 1.18 - tie to vega-datasets avoiding esmodules until ijavascript can support them
50
51
  * 1.17 - provide object.propertyValueSample - as a way to list 'non-empty' property values
51
52
  * 1.16 - provide file.matchFiles - as a way to find files or directories
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-ijavascript-utils",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Utilities for working with iJavaScript - a Jupyter Kernel",
5
5
  "homepage": "https://jupyter-ijavascript-utils.onrender.com/",
6
6
  "license": "MIT",
@@ -1008,7 +1008,7 @@ class TableGenerator {
1008
1008
  keys = keys.filter((key) => this.#columnsToExclude.indexOf(key) === -1);
1009
1009
 
1010
1010
  //-- identify the formatter to use
1011
- const cleanFormatter = this.#formatterFn ? this.#formatterFn : ({ value }) => value;
1011
+ const cleanFormatter = this.#formatterFn ? this.#formatterFn : ({ value }) => value === undefined ? '' : value;
1012
1012
 
1013
1013
  const translateHeader = (key) => {
1014
1014
  if (Object.prototype.hasOwnProperty.call(this.#labels, key)) {
@@ -0,0 +1,687 @@
1
+ /* eslint-disable max-classes-per-file, class-methods-use-this */
2
+
3
+ const FormatUtils = require('./format');
4
+ const ObjectUtils = require('./object');
5
+
6
+ /**
7
+ * Module to describe objects or sets of data
8
+ *
9
+ * Describe an array of objects
10
+ * * {@link module:describe.describeObjects|describeObjects(collection, options)} - given a list of objects, describes each of the fields
11
+ * Describe an array of values (assuming all are the same type)
12
+ * * {@link module:describe.describeBoolean|describeBoolean(collection, options)} - describes a series of booleans
13
+ * * {@link module:describe.describeStrings|describeStrings(collection, options)} - describes a series of strings
14
+ * * {@link module:describe.describeNumbers|describeNumbers(collection, options)} - describes a series of numbers
15
+ * * {@link module:describe.describeDates|describeDates(collection, options)} - describes a series of dates
16
+ *
17
+ * @module describe
18
+ * @exports describe
19
+ */
20
+ module.exports = {};
21
+ const DescribeUtil = module.exports;
22
+
23
+ /**
24
+ * @typedef {Object} DescribeOptions
25
+ * @property {Boolean} uniqueStrings - whether unique strings / frequency should be captured
26
+ */
27
+
28
+ /**
29
+ * Base Description for a series of values
30
+ * @class
31
+ */
32
+ class SeriesDescription {
33
+ /**
34
+ * Constructor
35
+ * @param {String} what - description of what is being described
36
+ * @param {DescribeOptions} options - options for how things are described
37
+ */
38
+ constructor(what, type, options) {
39
+ this.reset();
40
+ this.what = what;
41
+ this.type = type;
42
+ // this.options = options || {};
43
+ }
44
+
45
+ /**
46
+ * Options used for describing
47
+ * @type {DescribeOptions}
48
+ */
49
+ // options;
50
+
51
+ /**
52
+ * What is being described
53
+ * @type {String}
54
+ */
55
+ what;
56
+
57
+ /**
58
+ * The type of thing being described
59
+ * @type {String}
60
+ */
61
+ type;
62
+
63
+ /**
64
+ * The number of entries reviewed
65
+ * @type {Number}
66
+ */
67
+ count;
68
+
69
+ /**
70
+ * The minimum value found;
71
+ * @type {any}
72
+ */
73
+ min;
74
+
75
+ /**
76
+ * The maximum value found
77
+ * @type {any}
78
+ */
79
+ max;
80
+
81
+ /**
82
+ * Resets the Description to the initial state
83
+ */
84
+ reset() {
85
+ this.count = 0;
86
+ this.max = null;
87
+ this.min = null;
88
+ }
89
+
90
+ /**
91
+ * Validates a value is the type expected
92
+ * or throws an error if the type is not
93
+ * or throws false if the value is 'empty'
94
+ * @param {any} value - value to be checked
95
+ * @param {String} expectedTypeOf - the type of the value
96
+ * @returns {Boolean} - true if found and the right type, false if empty
97
+ * @throws {Error} if the value is the wrong type
98
+ */
99
+ check(value, expectedType) {
100
+ if (FormatUtils.isEmptyValue(value)) {
101
+ return false;
102
+ }
103
+
104
+ const valueType = typeof value;
105
+ if (expectedType && valueType !== expectedType) {
106
+ throw Error(`describe: Value passed(${value}) expected to be:${expectedType}, but was: ${valueType}`);
107
+ }
108
+
109
+ this.count += 1;
110
+
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Checks for minimum and maximum values
116
+ * @param {any} value
117
+ */
118
+ checkMinMax(value) {
119
+ if (this.min === null || value < this.min) {
120
+ this.min = value;
121
+ }
122
+ if (this.max === null || value > this.max) {
123
+ this.max = value;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Finalizes the review
129
+ */
130
+ finalize() { // eslint-disable-line
131
+ const result = { ...this };
132
+ // delete result.options;
133
+ return result;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Describes a series of Boolean Values
139
+ * @augments SeriesDescription
140
+ * @class
141
+ */
142
+ class BooleanDescription extends SeriesDescription {
143
+ /**
144
+ * Mean sum as expressed
145
+ * @type {number}
146
+ */
147
+ mean;
148
+
149
+ /**
150
+ *
151
+ * @param {String} what - what is being described
152
+ * @param {DescribeOptions} options - options used for describing
153
+ */
154
+ constructor(what, options) {
155
+ super(what, 'boolean', options);
156
+ this.reset();
157
+ }
158
+
159
+ reset() {
160
+ super.reset();
161
+ this.mean = 0.0;
162
+ }
163
+
164
+ /**
165
+ * Whether the value can be described with this
166
+ * @param {any} value - value to check
167
+ * @returns {Boolean} - true if the value matches
168
+ */
169
+ static matchesType(value) {
170
+ return FormatUtils.parseBoolean(value);
171
+ }
172
+
173
+ check(value) {
174
+ if (FormatUtils.isEmptyValue(value)) return;
175
+
176
+ this.count += 1;
177
+ const cleanValue = FormatUtils.parseBoolean(value)
178
+ ? 1 : 0;
179
+
180
+ const oldMean = this.mean;
181
+ this.mean += (cleanValue - oldMean) / this.count;
182
+
183
+ if (this.max === null && cleanValue === 1) this.max = 1;
184
+ if (this.min === null && cleanValue === 0) this.min = 0;
185
+ }
186
+
187
+ finalize() {
188
+ const result = super.finalize();
189
+ return result;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Describes a series of Numbers
195
+ */
196
+ class NumberDescription extends SeriesDescription {
197
+ /**
198
+ * Mean sum as expressed
199
+ * @type {number}
200
+ */
201
+ mean;
202
+
203
+ /**
204
+ * M2 - sum of squared deviation
205
+ */
206
+ m2;
207
+
208
+ /**
209
+ * Standard deviation of the numbers
210
+ */
211
+ stdDeviation;
212
+
213
+ /**
214
+ * Constructor
215
+ * @param {String} what - What is being described
216
+ * @param {DescribeOptions} options -
217
+ */
218
+ constructor(what, options) {
219
+ super(what, 'number', options);
220
+ this.reset();
221
+ }
222
+
223
+ reset() {
224
+ super.reset();
225
+ this.mean = 0.0;
226
+ this.m2 = 0.0;
227
+ this.stdDeviation = 0.0;
228
+ }
229
+
230
+ /**
231
+ * Whether the value can be described with this
232
+ * @param {any} value - value to check
233
+ * @returns {Boolean} - true if the value matches
234
+ */
235
+ static matchesType(value) {
236
+ return (typeof value) === 'number';
237
+ }
238
+
239
+ check(value) {
240
+ if (!super.check(value, 'number')) return;
241
+ super.checkMinMax(value);
242
+
243
+ /*
244
+ @see Welford's algorithm
245
+ @see https://stackoverflow.com/a/1348615
246
+ @see https://lingpipe-blog.com/2009/03/19/computing-sample-mean-variance-online-one-pass/
247
+ @see https://lingpipe-blog.com/2009/07/07/welford-s-algorithm-delete-online-mean-variance-deviation/
248
+ @see https://www.calculator.net/standard-deviation-calculator.html
249
+ */
250
+ const oldMean = this.mean;
251
+ this.mean += (value - oldMean) / this.count;
252
+ this.m2 += (value - oldMean) * (value - this.mean);
253
+ // console.log(`value:${value}, this.mean:${this.mean}, oldMean:${oldMean}, stdDeviation:${this.stdDeviation}`);
254
+ }
255
+
256
+ finalize() {
257
+ let newDeviation;
258
+ if (this.count > 1) {
259
+ newDeviation = Math.sqrt(this.m2 / this.count);
260
+ } else {
261
+ newDeviation = 0.0;
262
+ }
263
+ // console.log(`updated m2:${this.m2}, stdDeviation:${this.stdDeviation}, count:${this.count}, newDeviation:${newDeviation}`);
264
+ this.stdDeviation = newDeviation;
265
+
266
+ const result = super.finalize();
267
+ delete result.m2;
268
+ return result;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Describes a series of string values
274
+ */
275
+ class StringDescription extends SeriesDescription {
276
+ /**
277
+ * Map of unique values
278
+ * @type {Map<String,Number>}
279
+ */
280
+ uniqueMap;
281
+
282
+ /**
283
+ * Number of unique values;
284
+ * @type {Number}
285
+ */
286
+ unique;
287
+
288
+ /**
289
+ * The most common string
290
+ * @type {String}
291
+ */
292
+ top;
293
+
294
+ /**
295
+ * The frequency of the most common string
296
+ * @type {Number}
297
+ */
298
+ topFrequency;
299
+
300
+ /**
301
+ * Constructor
302
+ * @param {String} what - What is being described
303
+ * @param {DescribeOptions} options -
304
+ */
305
+ constructor(what, options) {
306
+ super(what, 'string', options);
307
+ this.uniqueMap = null;
308
+ this.reset();
309
+ }
310
+
311
+ reset() {
312
+ super.reset();
313
+ this.uniqueMap = new Map();
314
+ this.unique = null;
315
+ this.top = null;
316
+ this.topFrequency = null;
317
+ }
318
+
319
+ /**
320
+ * Whether the value can be described with this
321
+ * @param {any} value - value to check
322
+ * @returns {Boolean} - true if the value matches
323
+ */
324
+ static matchesType(value) {
325
+ return (typeof value) === 'string';
326
+ }
327
+
328
+ check(value) {
329
+ if (!super.check(value, 'string')) return;
330
+
331
+ if (this.uniqueMap.has(value)) {
332
+ this.uniqueMap.set(value, this.uniqueMap.get(value) + 1);
333
+ return;
334
+ }
335
+
336
+ this.uniqueMap.set(value, 1);
337
+
338
+ const len = value.length;
339
+ if (this.min === null || len < this.min.length) this.min = value;
340
+ if (this.max === null || len > this.max.length) this.max = value;
341
+ // console.log(`len:${len}, min:${this.min}, max:${this.max}`);
342
+ }
343
+
344
+ finalize() {
345
+ super.finalize();
346
+
347
+ let currentTop = null;
348
+ let currentTopFrequency = null;
349
+ for (const [key, count] of this.uniqueMap.entries()) {
350
+ if (currentTopFrequency == null || count > currentTopFrequency) {
351
+ currentTop = key;
352
+ currentTopFrequency = count;
353
+ }
354
+ }
355
+ this.top = currentTop;
356
+ this.topFrequency = currentTopFrequency;
357
+ this.unique = this.uniqueMap.size;
358
+
359
+ this.uniqueMap = null;
360
+
361
+ const result = super.finalize();
362
+ delete result.uniqueMap;
363
+ return result;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Describes a series of Dates
369
+ */
370
+ class DateDescription extends SeriesDescription {
371
+ /**
372
+ * Mean sum as expressed
373
+ * @type {number}
374
+ */
375
+ mean;
376
+
377
+ /**
378
+ * Constructor
379
+ * @param {String} what - What is being described
380
+ * @param {DescribeOptions} options -
381
+ */
382
+ constructor(what, options) {
383
+ super(what, 'Date', options);
384
+ this.reset();
385
+ }
386
+
387
+ reset() {
388
+ super.reset();
389
+ this.mean = null;
390
+ }
391
+
392
+ /**
393
+ * Whether the value can be described with this
394
+ * @param {any} value - value to check
395
+ * @returns {Boolean} - true if the value matches
396
+ */
397
+ static matchesType(value) {
398
+ return (value instanceof Date); // || (typeof value) === 'number';
399
+ }
400
+
401
+ check(value) {
402
+ if (FormatUtils.isEmptyValue(value)) return;
403
+
404
+ let cleanValue;
405
+ if (value instanceof Date) {
406
+ cleanValue = value.getTime();
407
+ } else if (typeof value === 'number') {
408
+ cleanValue = value;
409
+ } else {
410
+ throw Error(`describe: Value passed(${value}) - expected to be type:Date`);
411
+ }
412
+
413
+ this.count += 1;
414
+
415
+ const oldMean = this.mean;
416
+ this.mean += (cleanValue - oldMean) / this.count;
417
+
418
+ super.checkMinMax(cleanValue);
419
+ }
420
+
421
+ finalize() {
422
+ if (!FormatUtils.isEmptyValue(this.min)) this.min = new Date(this.min);
423
+ if (!FormatUtils.isEmptyValue(this.max)) this.max = new Date(this.max);
424
+ if (!FormatUtils.isEmptyValue(this.mean)) this.mean = new Date(this.mean);
425
+
426
+ const result = super.finalize();
427
+ return result;
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Describes a collection of objects.
433
+ *
434
+ * For example, given the following collection:
435
+ *
436
+ * ```
437
+ * collection = [{
438
+ * first: 'john',
439
+ * last: 'doe',
440
+ * age: 23,
441
+ * enrolled: new Date('2022-01-01')
442
+ * }, {
443
+ * first: 'john',
444
+ * last: 'doe',
445
+ * age: 24,
446
+ * enrolled: new Date('2022-01-03')
447
+ * }, {
448
+ * first: 'jan',
449
+ * last: 'doe',
450
+ * age: 25,
451
+ * enrolled: new Date('2022-01-05')
452
+ * }];
453
+ * ```
454
+ *
455
+ * Running `utils.describe.describeObjects(collection);` gives:
456
+ *
457
+ * ```
458
+ * [{
459
+ * "count": 3,
460
+ * "max": "john",
461
+ * "min": "jan",
462
+ * "top": "john",
463
+ * "topFrequency": 2,
464
+ * "type": "string",
465
+ * "unique": 2,
466
+ * "what": "first"
467
+ * }, {
468
+ * "count": 3,
469
+ * "max": "doe",
470
+ * "min": "doe",
471
+ * "top": "doe",
472
+ * "topFrequency": 3,
473
+ * "type": "string",
474
+ * "unique": 1,
475
+ * "what": "last"
476
+ * }, {
477
+ * "count": 3,
478
+ * "max": 25,
479
+ * "min": 23,
480
+ * "mean": 24,
481
+ * "stdDeviation": 0.816496580927726,
482
+ * "type": "number",
483
+ * "what": "age"
484
+ * }, {
485
+ * "count": 3,
486
+ * "max": "2022-01-05T00:00:00.000Z",
487
+ * "min": "2022-01-01T00:00:00.000Z",
488
+ * "mean": "2022-01-03T00:00:00.000Z",
489
+ * "type": "Date",
490
+ * "what": "enrolled"
491
+ * }]
492
+ * ```
493
+ *
494
+ * Or Rendered to a table: `utils.table(results).render()`:
495
+ *
496
+ * what |type |count|max |min |mean |top |topFrequency|unique
497
+ * -- |-- |-- |-- |-- |-- |-- |-- |--
498
+ * first |string|3 |john |jan | |john|2 |2
499
+ * last |string|3 |doe |doe | |doe |3 |1
500
+ * age |number|3 |25 |23 |24 | | |
501
+ * enrolled|Date |3 |2022-01-05T00:00:00.000Z|2022-01-01T00:00:00.000Z|2022-01-03T00:00:00.000Z| | |
502
+ *
503
+ * @param {Object[]} collection - Collection of objects to be described
504
+ * @param {Object} options - options to be used
505
+ * @param {String[]} options.include - string list of fields to include in the description
506
+ * @param {String[]} options.exclude - string list of fields to exclude in the description
507
+ * @param {Object} options.overridePropertyType - object with property:type values (string|number|date|boolean)
508
+ * - that will override how that property is parsed.
509
+ * @param {Number} maxRows - max rows to consider before halting
510
+ * @returns {SeriesDescription[]} - collection of descriptions - one for each property
511
+ */
512
+ module.exports.describeObjects = function describeObjects(collection, options) {
513
+ const cleanCollection = Array.isArray(collection) ? collection : [collection];
514
+
515
+ const cleanOptions = options ? options : {};
516
+
517
+ cleanOptions.include = cleanOptions.include ? new Set(cleanOptions.include) : null;
518
+ cleanOptions.exclude = new Set(cleanOptions.exclude || []);
519
+ cleanOptions.maxRows = cleanOptions.maxRows || -1;
520
+
521
+ // cleanOptions.prepareFn = typeof cleanOptions.prepareFn === 'function'
522
+ // ? cleanOptions.prepareFn
523
+ // : (val) => val;
524
+
525
+ const results = new Map();
526
+ if (cleanOptions.overridePropertyType) {
527
+ ObjectUtils.keys(cleanOptions.overridePropertyType)
528
+ .forEach((key) => {
529
+ const keyValue = cleanOptions.overridePropertyType[key];
530
+ if (keyValue === 'string') {
531
+ results.set(key, new StringDescription(key, cleanOptions));
532
+ } else if (keyValue === 'number') {
533
+ results.set(key, new NumberDescription(key, cleanOptions));
534
+ } else if (keyValue === 'date') {
535
+ results.set(key, new DateDescription(key, cleanOptions));
536
+ } else if (keyValue === 'boolean' || keyValue === 'bool') {
537
+ results.set(key, new StringDescription(key, cleanOptions));
538
+ }
539
+ });
540
+ }
541
+ let val;
542
+ // let describer;
543
+
544
+ cleanCollection.every((obj, index) => {
545
+ if (cleanOptions.maxRows > 0 && index >= cleanOptions.maxRows) {
546
+ return false;
547
+ }
548
+ //-- handles null objects
549
+ // obj = cleanOptions.prepareFn(obj);
550
+ ObjectUtils.keys(obj)
551
+ .forEach((key) => {
552
+ val = obj[key];
553
+
554
+ if (cleanOptions.include && !cleanOptions.include.has(key)) {
555
+ //-- ignore
556
+ } else if (cleanOptions.exclude.has(key)) {
557
+ //-- ignore
558
+ } else if (FormatUtils.isEmptyValue(val)) {
559
+ //-- do nothing
560
+ } else {
561
+ if (Object.prototype.hasOwnProperty.call(results, key)) {
562
+ //-- describer already found
563
+ } else if (StringDescription.matchesType(val)) {
564
+ results[key] = new StringDescription(key);
565
+ } else if (DateDescription.matchesType(val)) {
566
+ results[key] = new DateDescription(key);
567
+ } else if (NumberDescription.matchesType(val)) {
568
+ results[key] = new NumberDescription(key);
569
+ } else if (BooleanDescription.matchesType(val)) {
570
+ results[key] = new BooleanDescription(key);
571
+ } else {
572
+ //-- ignore?
573
+ results[key] = new SeriesDescription(key, typeof val);
574
+ }
575
+ results[key].check(val);
576
+ }
577
+ });
578
+ return true;
579
+ });
580
+
581
+ const resultArray = ObjectUtils.keys(results)
582
+ .map((key) => results[key].finalize());
583
+
584
+ return resultArray;
585
+ };
586
+
587
+ /**
588
+ * Describes a series of numbers
589
+ * @param {String[]} collection - collection of string values to describe
590
+ * @param {Object} options - options for describing strings
591
+ * @returns {StringDescription} - Description of the list of strings
592
+ */
593
+ module.exports.describeStrings = function describeStrings(collection, options) {
594
+ const cleanCollection = Array.isArray(collection) ? collection : [collection];
595
+
596
+ const result = new StringDescription(null, options);
597
+ cleanCollection.forEach((value) => result.check(value));
598
+ return result.finalize();
599
+ };
600
+
601
+ /**
602
+ * Describes a series of numbers
603
+ * @param {Number[]} collection - Array of numbers
604
+ * @param {Object} options - options for describing numbers
605
+ * @returns {NumberDescription}
606
+ */
607
+ module.exports.describeNumbers = function describeNumbers(collection, options) {
608
+ const cleanCollection = Array.isArray(collection) ? collection : [collection];
609
+
610
+ const result = new NumberDescription(null, options);
611
+ cleanCollection.forEach((value) => result.check(value));
612
+ return result.finalize();
613
+ };
614
+
615
+ /**
616
+ * Describes a series of boolean values.
617
+ *
618
+ * Note, that the following are considered TRUE:
619
+ *
620
+ * * Boolean true
621
+ * * Number 1
622
+ * * String TRUE
623
+ * * String True
624
+ * * String true
625
+ *
626
+ * @param {Boolean[] | String[] | Number[]} collection - Array of Boolean Values
627
+ * @param {Object} options - options for describing boolean values
628
+ * @returns {BooleanDescription}
629
+ * @see {@link module:format.parseBooleanValue}
630
+ */
631
+ module.exports.describeBoolean = function describeBoolean(collection, options) {
632
+ const cleanCollection = Array.isArray(collection) ? collection : [collection];
633
+
634
+ const result = new BooleanDescription(null, options);
635
+ cleanCollection.forEach((value) => result.check(value));
636
+ return result.finalize();
637
+ };
638
+
639
+ /**
640
+ * Describes a series of Date / Epoch Numbers
641
+ *
642
+ * @param {Date[] | Number[]} collection - Array of Dates / Epoch Numbers
643
+ * @param {Object} options - options for describing dates
644
+ * @returns {DateDescription}
645
+ */
646
+ module.exports.describeDates = function describeDates(collection, options) {
647
+ const cleanCollection = Array.isArray(collection) ? collection : [collection];
648
+
649
+ const result = new DateDescription(null, options);
650
+ cleanCollection.forEach((value) => result.check(value));
651
+ return result.finalize();
652
+ };
653
+
654
+ //-- Testing Internal items
655
+
656
+ /**
657
+ * Sanity check for standard deviation
658
+ * @param {Number[]} series - collection of numbers
659
+ * @returns {Number} - standard deviation of the numbers
660
+ * @private
661
+ */
662
+ DescribeUtil.stdDeviation = function stdDeviation(series) {
663
+ let avg = 0;
664
+
665
+ if (series.length < 2) return 0.0;
666
+
667
+ const sum = series.reduce((result, val) => result + val, 0);
668
+ avg = sum / series.length;
669
+
670
+ const s1 = series.reduce((result, val) => result + ((val - avg) ** 2), 0);
671
+ // console.log(`s1:${s1}`);
672
+ const s2 = Math.sqrt(s1 / series.length);
673
+
674
+ return s2;
675
+ };
676
+
677
+ /**
678
+ * Number Description - used for testing
679
+ * @private
680
+ */
681
+ DescribeUtil.NumberDescription = NumberDescription;
682
+
683
+ /**
684
+ * String Description - used for testing
685
+ * @private
686
+ */
687
+ DescribeUtil.StringDescription = StringDescription;
package/src/format.js CHANGED
@@ -15,6 +15,8 @@
15
15
  * * {@link module:format.capitalize|format.capitalize} - Capitalizes only the first character in the string (ex: 'John paul');
16
16
  * * {@link module:format.capitalizeAll|format.capitalizeAll} - Capitalizes all the words in a string (ex: 'John Paul')
17
17
  * * {@link module:format.ellipsify|format.ellipsify} - Truncates a string if the length is 'too long'
18
+ * * {@link module:format.limitLines|format.limitLines(string, toLine, fromLine, lineSeparator)} - selects only a subset of lines in a string
19
+ * * {@link module:format.consoleLines|format.consoleLines(...)} - same as limit lines, only console.logs the string out.
18
20
  * * Formatting Time
19
21
  * * {@link module:format.millisecondDuration|format.millisecondDuration}
20
22
  * * Mapping Values
@@ -844,3 +846,78 @@ module.exports.isEmptyValue = (val) =>
844
846
  //-- allow for 0s
845
847
  val === null || val === undefined || val === ''
846
848
  || (Array.isArray(val) && val.length === 0);
849
+
850
+ /**
851
+ * Determines if a value is a boolean true value.
852
+ *
853
+ * Matches for:
854
+ *
855
+ * * boolean TRUE
856
+ * * number 1
857
+ * * string 'TRUE'
858
+ * * string 'True'
859
+ * * string 'true'
860
+ *
861
+ * @param {any} val - the value to be tested
862
+ * @returns {Boolean} - TRUE if the value matches
863
+ */
864
+ module.exports.parseBoolean = function parseBoolean(val) {
865
+ return val === true
866
+ || val === 1
867
+ || val === 'TRUE'
868
+ || val === 'True'
869
+ || val === 'true';
870
+ };
871
+
872
+ /**
873
+ * Narrows to only fromLine - toLine (inclusive) within a string.
874
+ *
875
+ * @see {@link module:format.consoleLines|format.consoleLines()} - to console the values out
876
+ * @param {String|Object} str - string to be limited, or object to be json.stringify-ied
877
+ * @param {Number} toLine
878
+ * @param {Number} [fromLine=0] - starting line number (starts at 0)
879
+ * @param {String} [lineSeparator='\n'] - separator for lines
880
+ * @returns {String}
881
+ * @example
882
+ * str = '1\n2\n\3';
883
+ * utils.format.limitLines(str, 2); // '1\n2'
884
+ *
885
+ * str = '1\n2\n3';
886
+ * utils.format.limitLines(str, 3, 2); // '2\n3'
887
+ *
888
+ * str = '1\n2\n3';
889
+ * utils.format.limitLines(str, undefined, 2); // '2\n3'
890
+ */
891
+ module.exports.limitLines = function limitLines(str, toLine, fromLine, lineSeparator) {
892
+ const cleanStr = typeof str === 'string'
893
+ ? str
894
+ : JSON.stringify(str || '', FormatUtils.mapReplacer, 2);
895
+ const cleanLine = lineSeparator || '\n';
896
+
897
+ return cleanStr.split(cleanLine)
898
+ .slice(fromLine || 0, toLine)
899
+ .join(cleanLine);
900
+ };
901
+
902
+ /**
903
+ * Same as {@link module:format.limitLines|limitLines()} - only prints to the console.
904
+ *
905
+ * @see {@link module:format.limitLines|format.limitLines}
906
+ * @param {String|Object} str - string to be limited, or object to be json.stringify-ied
907
+ * @param {Number} toLine
908
+ * @param {Number} [fromLine=0] - starting line number (starts at 0)
909
+ * @param {String} [lineSeparator='\n'] - separator for lines
910
+ * @returns {String}
911
+ * @example
912
+ * str = '1\n2\n\3';
913
+ * utils.format.limitLines(str, 2); // '1\n2'
914
+ *
915
+ * str = '1\n2\n3';
916
+ * utils.format.limitLines(str, 3, 2); // '2\n3'
917
+ *
918
+ * str = '1\n2\n3';
919
+ * utils.format.limitLines(str, undefined, 2); // '2\n3'
920
+ */
921
+ module.exports.consoleLines = function consoleLines(str, toLine, fromLine, lineSeparator) {
922
+ console.log(FormatUtils.limitLines(str, toLine, fromLine, lineSeparator));
923
+ };
package/src/hashMap.js ADDED
@@ -0,0 +1,218 @@
1
+ const FormatUtils = require('./format');
2
+
3
+ /**
4
+ * Library for working with JavaScript hashmaps.
5
+ *
6
+ * * Modifying
7
+ * * {@link module:hashMap.add|hashMap.add(map, key, value):Map} - Add a value to a map and return the Map
8
+ * * {@link module:hashMap.union|hashMap.union(targetMap, additionalMap, canOverwrite)} - merges two maps and ignores or overwrites with conflicts
9
+ * * Cloning
10
+ * * {@link module:hashMap.clone|hashMap.clone(map):Map} - Clones a given Map
11
+ * * Conversion
12
+ * * {@link module:hashMap.stringify|hashMap.stringify(map, indent)} - converts a Map to a string representation
13
+ * * {@link module:hashMap.toObject|hashMap.toObject(map)} - converts a hashMap to an Object
14
+ * * {@link module:hashMap.fromObject|hashMap.fromObject(object)} - converts an object's properties to hashMap keys
15
+ *
16
+ * Note: JavaScript Maps can sometimes be faster than using Objects,
17
+ * and sometimes slower.
18
+ *
19
+ * (Current understanding is that Maps do better with more updates made)
20
+ *
21
+ * There are many searches such as `javascript map vs object performance`
22
+ * with many interesting links to come across.
23
+ *
24
+ * @module hashMap
25
+ * @exports hashMap
26
+ */
27
+ module.exports = {};
28
+ const HashMapUtil = module.exports;
29
+
30
+ /**
31
+ * Set a Map in a functional manner (adding a value and returning the map)
32
+ * @param {Map} map - the map to be updated
33
+ * @param {any} key -
34
+ * @param {any} value -
35
+ * @returns {Map} - the updated map value
36
+ * @example
37
+ * const objectToMap = { key1: 1, key2: 2, key3: 3 };
38
+ * const keys = [...Object.keys(objectToMap)];
39
+ * // ['key1', 'key2', 'key3'];
40
+ *
41
+ * const result = keys.reduce(
42
+ * (result, key) => utils.hashMap.add(result, key, objectToMap[key]),
43
+ * new Map()
44
+ * );
45
+ * // Map([[ 'key1',1 ], ['key2', 2], ['key3', 3]]);
46
+ */
47
+ module.exports.add = function add(map, key, value) {
48
+ map.set(key, value);
49
+ return map;
50
+ };
51
+
52
+ /**
53
+ * Clones a Map
54
+ * @param {Map} target - Map to clone
55
+ * @returns {Map} - clone of the target map
56
+ * @example
57
+ * const sourceMap = new Map();
58
+ * sourceMap.set('first', 1);
59
+ * const mapClone = utils.hashMap.clone(sourceMap);
60
+ * mapClone.has('first'); // true
61
+ */
62
+ module.exports.clone = function clone(target) {
63
+ if (!(target instanceof Map)) {
64
+ throw Error('hashMap.clone(targetMap): targetMap must be a Map');
65
+ }
66
+ return new Map(target.entries());
67
+ };
68
+
69
+ /**
70
+ * Creates a new map that includes all entries of targetMap, and all entries of additionalMap.
71
+ *
72
+ * If allowOverwrite is true, then values found in additionalMap will take priority in case of conflicts.
73
+ *
74
+ * ```
75
+ * const targetMap = new Map([['first', 'John'], ['amount': 100]]);
76
+ * const additionalMap = new Map([['last': 'Doe'], ['amount': 200]]);
77
+ *
78
+ * utils.hashMap.union(targetMap, additionalMap, true);
79
+ * // Map([['first', 'John'], ['last', 'Doe'], ['amount', 200]]);
80
+ * ```
81
+ *
82
+ * If allowOverwrite is false, then values found in targetMap will take priority in case of conflicts.
83
+ *
84
+ * ```
85
+ * const targetMap = new Map([['first', 'John'], ['amount': 100]]);
86
+ * const additionalMap = new Map([['last': 'Doe'], ['amount': 200]]);
87
+ *
88
+ * utils.hashMap.union(targetMap, additionalMap);
89
+ * utils.hashMap.union(targetMap, additionalMap, false);
90
+ * // Map([['first', 'John'], ['last', 'Doe'], ['amount', 100]]);
91
+ * ```
92
+ *
93
+ * @param {Map} targetMap
94
+ * @param {Map} additionalMap -
95
+ * @param {Boolean} [allowOverwrite=false] - whether targetMap is prioritized (false) or additional prioritized (true)
96
+ * @returns {Map}
97
+ */
98
+ module.exports.union = function union(targetMap, additionalMap, allowOverwrite) {
99
+ if (!(targetMap instanceof Map)) {
100
+ return HashMapUtil.clone(additionalMap);
101
+ }
102
+
103
+ const result = new Map(targetMap.entries());
104
+
105
+ if (!(additionalMap instanceof Map)) {
106
+ return result;
107
+ }
108
+
109
+ for (const key of additionalMap.keys()) {
110
+ if (!result.has(key) || allowOverwrite) {
111
+ result.set(key, additionalMap.get(key));
112
+ }
113
+ }
114
+ return result;
115
+ };
116
+
117
+ /**
118
+ * Serializes a hashMap (plain javascript Map) to a string
119
+ *
120
+ * ```
121
+ * const target = new Map([['first', 1], ['second', 2]]);
122
+ * HashMapUtil.stringify(target);
123
+ * // '{"dataType":"Map","value":[["first",1],["second",2]]}'
124
+ * ```
125
+ *
126
+ * Note, that passing indent will make the results much more legible.
127
+ *
128
+ * ```
129
+ * {
130
+ * "dataType": "Map",
131
+ * "value": [
132
+ * [
133
+ * "first",
134
+ * 1
135
+ * ],
136
+ * [
137
+ * "second",
138
+ * 2
139
+ * ]
140
+ * ]
141
+ * }
142
+ * ```
143
+ * @param {Map} target - the Map to be serialized
144
+ * @param {Number} indentation - the indentation passed to JSON.serialize
145
+ * @returns {String} - JSON.stringify string for the map
146
+ */
147
+ module.exports.stringify = function stringify(map, indentation) {
148
+ return JSON.stringify(map, FormatUtils.mapReplacer, indentation);
149
+ };
150
+
151
+ /**
152
+ * Converts a map to an object
153
+ *
154
+ * For example, say we have a Map:
155
+ *
156
+ * ```
157
+ * const targetMap = new Map([['first', 1], ['second', 2], ['third', 3]]);
158
+ * ```
159
+ *
160
+ * We can convert it to an Object as follows:
161
+ *
162
+ * ```
163
+ * const targetMap = utils.hashMap.toObject(targetObject)
164
+ * // { first: 1, second: 2, third: 3 };
165
+ * ```
166
+ *
167
+ * @param {Map} target - map to be converted
168
+ * @returns {Object} - object with the properties as the target map's keys.
169
+ * @see {@link hashMap.fromObject} - to reverse the process
170
+ */
171
+ module.exports.toObject = function toObject(target) {
172
+ const results = {};
173
+
174
+ if (!target) { // eslint-disable-line no-empty
175
+ } else if (!(target instanceof Map)) {
176
+ throw Error('hashMap.toObject(map): must be passed a Map');
177
+ } else {
178
+ [...target.keys()]
179
+ .forEach((key) => {
180
+ results[key] = target.get(key);
181
+ });
182
+ }
183
+
184
+ return results;
185
+ };
186
+
187
+ /**
188
+ * Creates a Map from the properties of an Object
189
+ *
190
+ * For example, say we have an object:
191
+ *
192
+ * ```
193
+ * const targetObject = { first: 1, second: 2, third: 3 };
194
+ * ```
195
+ *
196
+ * We can convert it to a Map as follows:
197
+ *
198
+ * ```
199
+ * const targetMap = utils.hashMap.fromObject(targetObject)
200
+ * // new Map([['first', 1], ['second', 2], ['third', 3]]);
201
+ * ```
202
+ *
203
+ * @param {Object} target - target object with properties that should be considered keys
204
+ * @returns {Map<String,any>} - converted properties as keys in a new map
205
+ * @see {@link hashMap.toObject} - to reverse the process
206
+ */
207
+ module.exports.fromObject = function fromObject(target) {
208
+ if (!(typeof target === 'object')) {
209
+ throw Error('hashMap.fromObject(object): must be passed an object');
210
+ }
211
+
212
+ if (target.dataType === 'Map' && Array.isArray(target.value)) {
213
+ return new Map(target.value);
214
+ }
215
+
216
+ return [...Object.keys(target)]
217
+ .reduce((result, key) => HashMapUtil.add(result, key, target[key]), new Map());
218
+ };
package/src/index.js CHANGED
@@ -2,7 +2,9 @@ const aggregate = require('./aggregate');
2
2
  const array = require('./array');
3
3
  const base64 = require('./base64');
4
4
  const datasets = require('./datasets');
5
+ const describe = require('./describe');
5
6
  const group = require('./group');
7
+ const hashMap = require('./hashMap');
6
8
  const ijsUtils = require('./ijs');
7
9
  const file = require('./file');
8
10
  const vega = require('./vega');
@@ -29,39 +31,43 @@ const table = function table(...rest) {
29
31
  * @private
30
32
  */
31
33
  module.exports = {
32
- /** @see module:aggregate */
34
+ /** @see {@link module:aggregate} */
33
35
  aggregate,
34
36
  agg: aggregate,
35
- /** @see module:array */
37
+ /** @see {@link module:array} */
36
38
  array,
37
- /** @see module:base64 */
39
+ /** @see {@link module:base64} */
38
40
  base64,
39
- /** @see module:datasets */
41
+ /** @see {@link module:datasets} */
40
42
  datasets,
41
43
  dataset: datasets,
42
- /** @see module:file */
44
+ /** @see {@link module:describe} */
45
+ describe,
46
+ /** @see {@link module:file} */
43
47
  file,
44
- /** @see module:group */
48
+ /** @see {@link module:group} */
45
49
  group,
46
- /** @see module:format */
50
+ /** @see {@link module:hashMap} */
51
+ hashMap,
52
+ /** @see {@link module:format} */
47
53
  format,
48
- /** @see IJSUtils */
54
+ /** @see {@link IJSUtils} */
49
55
  ijs: ijsUtils,
50
- /** @see module:latex */
56
+ /** @see {@link module:latex} */
51
57
  latex,
52
- /** @see module:leaflet */
58
+ /** @see {@link module:leaflet} */
53
59
  leaflet,
54
- /** @see module:object */
60
+ /** @see {@link module:object} */
55
61
  object,
56
62
  /** @see {@link module:plantuml} */
57
63
  plantuml,
58
- /** @see module:random */
64
+ /** @see {@link module:random} */
59
65
  random,
60
- /** @see module:set */
66
+ /** @see {@link module:set} */
61
67
  set,
62
- /** @see module:svg */
68
+ /** @see {@link module:svg} */
63
69
  svg,
64
- /** @see module:vega */
70
+ /** @see {@link module:vega} */
65
71
  vega,
66
72
 
67
73
  /** @see SourceMap */