jupyter-ijavascript-utils 1.30.0 → 1.32.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
@@ -75,6 +75,8 @@ Give it a try here:
75
75
 
76
76
  ## What's New
77
77
 
78
+ * 1.32 - Array.indexify to identify sections within a 1d array into a hierarchy.
79
+ * 1.31 - harden Array.transpose for arrays with nulls, and Table.generateTSV
78
80
  * 1.30 - add Format.wordWrap and Format.lineCount
79
81
  * 1.29 - Updated TableGenerator.format method
80
82
  * 1.28 - Sticky table headers for table.render
package/Dockerfile CHANGED
@@ -1,3 +1,3 @@
1
1
  # syntax=docker/dockerfile:1
2
2
 
3
- FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.30.0
3
+ FROM darkbluestudios/jupyter-ijavascript-utils:binder_1.32.0
package/README.md CHANGED
@@ -55,6 +55,8 @@ This is not intended to be the only way to accomplish many of these tasks, and a
55
55
 
56
56
  # What's New
57
57
 
58
+ * 1.32 - Array.indexify to identify sections within a 1d array into a hierarchy.
59
+ * 1.31 - harden Array.transpose for arrays with nulls, and Table.generateTSV
58
60
  * 1.30 - add Format.wordWrap and Format.lineCount
59
61
  * 1.29 - Updated TableGenerator.format method
60
62
  * 1.28 - Sticky table headers for table.render
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-ijavascript-utils",
3
- "version": "1.30.0",
3
+ "version": "1.32.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",
@@ -40,7 +40,7 @@
40
40
  "eslint-plugin-import": "^2.26.0",
41
41
  "eslint-watch": "^7.0.0",
42
42
  "jest": "^27.0.6",
43
- "jsdoc": "^3.6.10",
43
+ "jsdoc": "^4.0.2",
44
44
  "sinon": "^11.1.1"
45
45
  },
46
46
  "dependencies": {
@@ -135,10 +135,14 @@ const { createSort } = require('./array');
135
135
  * * {@link TableGenerator#generateHTML|generateHTML()} - returns html table with the results
136
136
  * * {@link TableGenerator#generateMarkdown|generateMarkdown()} - returns markdown with the results
137
137
  * * {@link TableGenerator#generateCSV|generateCSV()} - generates a CSV with the results
138
+ * * {@link TableGenerator#generateCSV|generateCSV()} - generates a TSV with the results
138
139
  * * {@link TableGenerator#generateArray|generateArray()} - generates an array of headers and data for further process
139
140
  * * {@link TableGenerator#generateArray2|generateArray2()} - generates a single array for further process
140
141
  * * render in jupyter
141
142
  * * {@link TableGenerator#render|render()} - renders the results in a table within jupyter
143
+ * * {@link TableGenerator#renderCSV|renderCSV()} - renders the generateCSV results in a table within jupyter
144
+ * * {@link TableGenerator#renderTSV|renderTSV()} - renders the generateTSV results in a table within jupyter
145
+ * * {@link TableGenerator#renderMarkdown|renderMarkdown()} - renders the generateMarkdown results in a table within jupyter
142
146
  *
143
147
  */
144
148
  class TableGenerator {
@@ -1398,6 +1402,43 @@ class TableGenerator {
1398
1402
  return tableResults;
1399
1403
  }
1400
1404
 
1405
+ /**
1406
+ * Generates a TSV Table
1407
+ * @see {@link TableGenerator#renderCSV}
1408
+ */
1409
+ generateTSV() {
1410
+ const results = this.prepare();
1411
+
1412
+ const printOptions = this.#printOptions;
1413
+
1414
+ const escapeString = (val) => {
1415
+ //-- always return as string values to preserve formatting
1416
+ return `"${
1417
+ printValue(val, printOptions)
1418
+ .replace(/"/g, '""')
1419
+ }"`;
1420
+ };
1421
+ const tsvify = (a) => a.map(escapeString)
1422
+ .join('\t');
1423
+
1424
+ const printHeader = (headers, style) => tsvify(headers)
1425
+ + '\n';
1426
+
1427
+ const printBody = (collection) => collection
1428
+ .map((dataRow) => tsvify(dataRow))
1429
+ .join('\n');
1430
+
1431
+ // const cleanFn = printValue;
1432
+ // .map((dataRow) => tsvify(dataRow.map((value) =>
1433
+ // cleanFn(value, printOptions))
1434
+ // )).join('\n');
1435
+
1436
+ const tableResults = printHeader(results.headers, '')
1437
+ + printBody(results.data);
1438
+
1439
+ return tableResults;
1440
+ }
1441
+
1401
1442
  /**
1402
1443
  * @typedef {Object} TableArray
1403
1444
  * @property {String} headers -
@@ -1601,6 +1642,44 @@ class TableGenerator {
1601
1642
 
1602
1643
  context.console.log(this.generateCSV());
1603
1644
  }
1645
+
1646
+ /**
1647
+ * Renders Markdown in the cell results
1648
+ * @see {@link TableGenerator#generateTSV}
1649
+ * @example
1650
+ * weather = [
1651
+ * { id: 1, city: 'Seattle', month: 'Aug', precip: 0.87 },
1652
+ * { id: 0, city: 'Seattle', month: 'Apr', precip: 2.68 },
1653
+ * { id: 2, city: 'Seattle', month: 'Dec', precip: 5.31 },
1654
+ * { id: 3, city: 'New York', month: 'Apr', precip: 3.94 },
1655
+ * { id: 4, city: 'New York', month: 'Aug', precip: 4.13 },
1656
+ * { id: 5, city: 'New York', month: 'Dec', precip: 3.58 },
1657
+ * { id: 6, city: 'Chicago', month: 'Apr', precip: 3.62 },
1658
+ * { id: 8, city: 'Chicago', month: 'Dec', precip: 2.56 },
1659
+ * { id: 7, city: 'Chicago', month: 'Aug', precip: 3.98 }
1660
+ * ];
1661
+ * utils.table(weather)
1662
+ * .renderTSV();
1663
+ *
1664
+ * // "id","city","month","precip"
1665
+ * // "1","Seattle","Aug","0.87"
1666
+ * // "0","Seattle","Apr","2.68"
1667
+ * // "2","Seattle","Dec","5.31"
1668
+ * // "3","New York","Apr","3.94"
1669
+ * // "4","New York","Aug","4.13"
1670
+ * // "5","New York","Dec","3.58"
1671
+ * // "6","Chicago","Apr","3.62"
1672
+ * // "8","Chicago","Dec","2.56"
1673
+ * // "7","Chicago","Aug","3.98"
1674
+ */
1675
+ renderTSV() {
1676
+ const context = IJSUtils.detectContext();
1677
+ if (!context) {
1678
+ throw (Error('Not in iJavaScript, no $$ variable available'));
1679
+ }
1680
+
1681
+ context.console.log(this.generateTSV());
1682
+ }
1604
1683
  }
1605
1684
 
1606
1685
  module.exports = TableGenerator;
package/src/array.js CHANGED
@@ -17,6 +17,7 @@ require('./_types/global');
17
17
  * * {@link module:array.createSort|array.createSort(sortIndex, sortIndex, ...)} - generates a sorting function
18
18
  * * {@link module:array.SORT_ASCENDING|array.SORT_ASCENDING} - common ascending sorting function for array.sort()
19
19
  * * {@link module:array.SORT_DESCENDING|array.SORT_DESCENDING} - common descending sorting function for array.sort()
20
+ * * {@link module:array.indexify|array.indexify} - identify sections within a 1d array to create a hierarchy.
20
21
  * * Rearrange Array
21
22
  * * {@link module:array.reshape|array.reshape} - reshapes an array to a size of rows and columns
22
23
  * * {@link module:array.transpose|array.transpose} - transposes (flips - the array along the diagonal)
@@ -26,6 +27,8 @@ require('./_types/global');
26
27
  * * {@link module:array.pickRows|array.pickRows} - picks a row from a 2d array
27
28
  * * {@link module:array.pickColumns|array.pickColumns} - picks a column from a 2d array
28
29
  * * {@link module:array.pick|array.pick} - picks either/or rows and columns
30
+ * * Understanding Values
31
+ * * {@link module:array.isMultiDimensional|array.isMultiDimensional} - determines if an array is multi-dimensional
29
32
  *
30
33
  * @module array
31
34
  * @exports array
@@ -300,6 +303,42 @@ module.exports.arrange = function arange(len, start = 0, step = 1) {
300
303
  */
301
304
  module.exports.arange = module.exports.arrange;
302
305
 
306
+ /**
307
+ * Determine whether an array is multi-dimensional (an array of arrays)
308
+ *
309
+ * For example:
310
+ *
311
+ * ```
312
+ * utils.array.isMultiDimensional(0); // false
313
+ * utils.array.isMultiDimensional([0,1,2,3]); // false
314
+ * utils.array.isMultiDimensional([[0,1], [2,3]]); // true
315
+ * utils.array.isMultiDimensional([0, [1,2]]); // true
316
+ * ```
317
+ *
318
+ * @param {Array} targetArray - array to check if multi-dimensional
319
+ * @returns {Boolean} - if the targetArray has any values that are multi-dimensional
320
+ */
321
+ module.exports.isMultiDimensional = function isMultiDimensional(targetArray) {
322
+ if (!targetArray || !Array.isArray(targetArray)) {
323
+ return false;
324
+ }
325
+ return targetArray.find((v) => Array.isArray(v)) !== undefined;
326
+ };
327
+
328
+ /**
329
+ * Determines the depth of a two dimensional array
330
+ * @param {Array} targetArray - two dimensional array
331
+ * @returns {Number}
332
+ * @private
333
+ */
334
+ module.exports.arrayLength2d = function arrayLength2d(targetArray) {
335
+ return (targetArray || [])
336
+ .reduce((max, line) => {
337
+ const len = (line || []).length;
338
+ return (len > max) ? len : max;
339
+ }, 0);
340
+ };
341
+
303
342
  /**
304
343
  * Transposes a two dimensional array, so an NxM becomes MxN
305
344
  * @param {any[]} matrix - MxN array
@@ -329,7 +368,7 @@ module.exports.transpose = function transpose(matrix) {
329
368
 
330
369
  //-- for speed, we use for loops.
331
370
  const rows = matrix.length;
332
- const cols = matrix[0].length;
371
+ const cols = ArrayUtils.arrayLength2d(matrix); // matrix[0].length;
333
372
  let colI;
334
373
  let rowI;
335
374
 
@@ -467,4 +506,180 @@ module.exports.arrangeMulti = function arangeMulti(...dimensions) {
467
506
  };
468
507
  module.exports.arangeMulti = module.exports.arrangeMulti;
469
508
 
470
- //-- collection utilities
509
+ /**
510
+ * Create a unique number index for each element in an array,
511
+ * alternatively using additional functions to indicate hierarchies of data.
512
+ *
513
+ * For example, markdown can be considered a hierarchy of data:
514
+ *
515
+ * ```
516
+ * markdownList = `# Overview
517
+ * This entire list is a hierarchy of data.
518
+ *
519
+ * # Section A
520
+ * This describes section A
521
+ *
522
+ * ## SubSection 1
523
+ * With a subsection belonging to Section A
524
+ *
525
+ * ## SubSection 2
526
+ * And another subsection sibling to SubSection 1, but also under Section A.
527
+ *
528
+ * # Section B
529
+ * With an entirely unrelated section B, that is sibling to Section A
530
+ *
531
+ * ## SubSection 1
532
+ * And another subsection 1, but this time related to Section B.`;
533
+ * ```
534
+ *
535
+ * And we want to convert this 1d array into a hierarchy.
536
+ *
537
+ * ```
538
+ * data = markdownList.split('\n')
539
+ * .filter(line => line ? true : false); // check for empty lines
540
+ *
541
+ * utils.format.consoleLines( data, 4);
542
+ * // ['# Overview',
543
+ * // 'This entire list is a hierarchy of data.',
544
+ * // '# Section A',
545
+ * // 'This describes section A',;
546
+ *
547
+ * //-- functions that return True if we are in a new "group"
548
+ * isHeader1 = (str) => str.startsWith('# ');
549
+ *
550
+ * isHeader1('# Overview'); // true
551
+ * isHeader1('This entire list is a hierarchy of data'); // false
552
+ * isHeader1('# Section A'); // true
553
+ * isHeader1('This describes section A'); // false
554
+ *
555
+ * indexedData = utils.array.indexify(data, isHeader1);
556
+ * [
557
+ * { entry: 'Heading', section: [ 0 ], subIndex: 1 },
558
+ * { entry: '# Overview', section: [ 1 ], subIndex: 0 },
559
+ * {
560
+ * entry: 'This entire list is a hierarchy of data.',
561
+ * section: [ 1 ],
562
+ * subIndex: 1
563
+ * },
564
+ * { entry: '# Section A', section: [ 2 ], subIndex: 0 },
565
+ * { entry: 'This describes section A', section: [ 2 ], subIndex: 1 },
566
+ * { entry: '## SubSection 1', section: [ 2 ], subIndex: 2 },
567
+ * {
568
+ * entry: 'With a subsection belonging to Section A',
569
+ * section: [ 2 ],
570
+ * subIndex: 3
571
+ * },
572
+ * { entry: '## SubSection 2', section: [ 2 ], subIndex: 4 },
573
+ * {
574
+ * entry: 'And another subsection sibling to SubSection 1, but also under Section A.',
575
+ * section: [ 2 ],
576
+ * subIndex: 5
577
+ * },
578
+ * { entry: '# Section B', section: [ 3 ], subIndex: 0 },
579
+ * {
580
+ * entry: 'With an entirely unrelated section B, that is sibling to Section A',
581
+ * section: [ 3 ],
582
+ * subIndex: 1
583
+ * },
584
+ * { entry: '## SubSection 1', section: [ 3 ], subIndex: 2 },
585
+ * {
586
+ * entry: 'And another subsection 1, but this time related to Section B.',
587
+ * section: [ 3 ],
588
+ * subIndex: 3
589
+ * }
590
+ * ];
591
+ * ```
592
+ *
593
+ * Note that this only indexes elements by the first header.
594
+ *
595
+ * To index this with two levels of hierarchy, we can pass another function.
596
+ *
597
+ * ```
598
+ * isHeader2 = (str) => str.startsWith('## ');
599
+ *
600
+ * isHeader2('# Overview'); // false
601
+ * isHeader2('This entire list is a hierarchy of data'); // false
602
+ * isHeader2('# Section A'); // true
603
+ * isHeader2('This describes section A'); // false
604
+ *
605
+ * indexedData = utils.array.indexify(data, isHeader1, isHeader2);
606
+ * // [
607
+ * // { entry: 'Heading', section: [ 0, 0 ], subIndex: 1 },
608
+ * // { entry: '# Overview', section: [ 1, 0 ], subIndex: 0 },
609
+ * // {
610
+ * // entry: 'This entire list is a hierarchy of data.',
611
+ * // section: [ 1, 0 ],
612
+ * // subIndex: 1
613
+ * // },
614
+ * // { entry: '# Section A', section: [ 2, 0 ], subIndex: 0 },
615
+ * // { entry: 'This describes section A', section: [ 2, 0 ], subIndex: 1 },
616
+ * // { entry: '## SubSection 1', section: [ 2, 1 ], subIndex: 0 },
617
+ * // {
618
+ * // entry: 'With a subsection belonging to Section A',
619
+ * // section: [ 2, 1 ],
620
+ * // subIndex: 1
621
+ * // },
622
+ * // { entry: '## SubSection 2', section: [ 2, 2 ], subIndex: 0 },
623
+ * // {
624
+ * // entry: 'And another subsection sibling to SubSection 1, but also under Section A.',
625
+ * // section: [ 2, 2 ],
626
+ * // subIndex: 1
627
+ * // },
628
+ * // { entry: '# Section B', section: [ 3, 0 ], subIndex: 0 },
629
+ * // {
630
+ * // entry: 'With an entirely unrelated section B, that is sibling to Section A',
631
+ * // section: [ 3, 0 ],
632
+ * // subIndex: 1
633
+ * // },
634
+ * // { entry: '## SubSection 1', section: [ 3, 1 ], subIndex: 0 },
635
+ * // {
636
+ * // entry: 'And another subsection 1, but this time related to Section B.',
637
+ * // section: [ 3, 1 ],
638
+ * // subIndex: 1
639
+ * // }
640
+ * // ];
641
+ * ```
642
+ */
643
+ module.exports.indexify = function indexify(source, ...sectionIndicatorFunctions) {
644
+ const functionSignature = 'indexify(source, ...sectionIndicatorFunctions)';
645
+
646
+ const counters = new Array(sectionIndicatorFunctions.length).fill(0);
647
+ let subIndex = 0;
648
+ // counters[counters.length - 1] = -1;
649
+
650
+ //-- validate inputs
651
+ if (!Array.isArray(source)) {
652
+ throw new Error(`${functionSignature}: source must be an array`);
653
+ }
654
+ sectionIndicatorFunctions.forEach((fn) => {
655
+ if (typeof fn !== 'function') {
656
+ throw new Error(`${functionSignature}: all section indicators passed must be functions`);
657
+ }
658
+ });
659
+
660
+ const results = source.map((entry) => {
661
+ let isNewSectionTripped = false;
662
+
663
+ sectionIndicatorFunctions.forEach((fn, index) => {
664
+ if (isNewSectionTripped) {
665
+ counters[index] = 0;
666
+ } else {
667
+ isNewSectionTripped = fn(entry) ? true : false;
668
+
669
+ if (isNewSectionTripped) {
670
+ counters[index] += 1;
671
+ }
672
+ }
673
+ });
674
+
675
+ if (isNewSectionTripped) {
676
+ subIndex = 0;
677
+ } else {
678
+ subIndex += 1;
679
+ }
680
+
681
+ return ({ entry, section: [...counters], subIndex });
682
+ });
683
+
684
+ return results;
685
+ };