linny-r 2.0.7 → 2.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -1843,6 +1843,8 @@ Each line should represent the points of a different bound line.">
1843
1843
  title="Rename selected dataset">
1844
1844
  <img id="ds-clone-btn" class="btn disab" src="images/clone.png"
1845
1845
  title="Clone selected dataset">
1846
+ <img id="ds-load-btn" class="btn enab" src="images/open.png"
1847
+ title="Load dataset(s) from CSV file">
1846
1848
  <img id="ds-delete-btn" class="btn disab" src="images/delete.png"
1847
1849
  title="Delete selected dataset"
1848
1850
  style="position: absolute; right: 2px">
@@ -1940,6 +1942,19 @@ Start with = to find exact match, with ~ to match first characters">
1940
1942
  <input id="rename-dataset-name" type="text" autocomplete="off">
1941
1943
  </div>
1942
1944
  </div>
1945
+
1946
+ <!-- the LOAD CSV dialog prompts the user for a CSV file on disk
1947
+ to be loaded -->
1948
+ <div id="load-csv-modal" class="modal">
1949
+ <div id="load-csv-dlg" class="inp-dlg">
1950
+ <div class="dlg-title">
1951
+ Load datasets from CSV file
1952
+ <img class="cancel-btn" src="images/cancel.png">
1953
+ <img class="ok-btn" src="images/ok.png">
1954
+ </div>
1955
+ <input id="load-csv-file" type="file">
1956
+ </div>
1957
+ </div>
1943
1958
 
1944
1959
  <!-- the NEW SELECTOR dialog prompts for a new selector for a dataset -->
1945
1960
  <div id="new-selector-modal" class="modal">
@@ -2495,7 +2510,7 @@ NOTE: * and ? will be interpreted as wildcards"
2495
2510
  <div id="experiment-separator"></div>
2496
2511
  <div id="experiment-default-message">
2497
2512
  To define a meaningful experiment, a model must feature at least
2498
- one outcome dataset, or a graph having one or more variables.
2513
+ one outcome dataset, or a chart having one or more variables.
2499
2514
  </div>
2500
2515
  <div id="experiment-params-header">(no experiment selected)</div>
2501
2516
  <div id="experiment-params-div">
@@ -794,6 +794,7 @@ div.checked.not-same-not-changed {
794
794
 
795
795
  /* LOAD modal dialog */
796
796
  #load-dlg,
797
+ #load-csv-dlg,
797
798
  #comparison-dlg {
798
799
  width: 430px;
799
800
  height: min-content;
@@ -801,6 +802,7 @@ div.checked.not-same-not-changed {
801
802
  }
802
803
 
803
804
  #load-xml-file,
805
+ #load-csv-file,
804
806
  #comparison-xml-file {
805
807
  padding: 2px;
806
808
  max-width: 375px;
@@ -446,7 +446,8 @@ class Controller {
446
446
  }
447
447
  // NOTE: Replace single quotes by Unicode apostrophe so that they
448
448
  // cannot interfere with JavaScript strings delimited by single quotes.
449
- return name.toLowerCase().replace(/\s/g, '_').replace("'", '\u2019');
449
+ return name.toLowerCase().replace(/\s/g, '_')
450
+ .replace("'", '\u2019').replace('"', '\uff02');
450
451
  }
451
452
 
452
453
  htmlEquationName(n) {
@@ -1003,10 +1003,13 @@ class GUIChartManager extends ChartManager {
1003
1003
  if(v.visible) {
1004
1004
  // NOTE: while still solving, display t-1 as N
1005
1005
  const n = Math.max(0, v.N);
1006
- html.push('<tr><td class="v-name">',
1007
- [v.displayName, n, data[nr][0], data[nr][1], data[nr][2],
1008
- data[nr][3], data[nr][4],
1009
- v.non_zero_tally, v.exceptions].join('</td><td>'),
1006
+ html.push('<tr><td class="v-name">', v.displayName, '</td><td>', n,
1007
+ '</td><td title="', v.minimum.toPrecision(8), '">', data[nr][0],
1008
+ '</td><td title="', v.maximum.toPrecision(8), '">', data[nr][1],
1009
+ '</td><td title="', v.mean.toPrecision(8), '">', data[nr][2],
1010
+ '</td><td title="', Math.sqrt(v.variance).toPrecision(8), '">', data[nr][3],
1011
+ '</td><td title="', v.sum.toPrecision(8), '">', data[nr][4],
1012
+ '</td><td>', v.non_zero_tally, '</td><td>', v.exceptions,
1010
1013
  '</td></tr>');
1011
1014
  nr++;
1012
1015
  }
@@ -52,6 +52,8 @@ class GUIDatasetManager extends DatasetManager {
52
52
  'click', () => DATASET_MANAGER.promptForName());
53
53
  document.getElementById('ds-clone-btn').addEventListener(
54
54
  'click', () => DATASET_MANAGER.cloneDataset());
55
+ document.getElementById('ds-load-btn').addEventListener(
56
+ 'click', () => DATASET_MANAGER.load_csv_modal.show());
55
57
  document.getElementById('ds-delete-btn').addEventListener(
56
58
  'click', () => DATASET_MANAGER.deleteDataset());
57
59
  document.getElementById('ds-filter-btn').addEventListener(
@@ -97,6 +99,11 @@ class GUIDatasetManager extends DatasetManager {
97
99
  'click', () => DATASET_MANAGER.renameDataset());
98
100
  this.rename_modal.cancel.addEventListener(
99
101
  'click', () => DATASET_MANAGER.rename_modal.hide());
102
+ this.load_csv_modal = new ModalDialog('load-csv');
103
+ this.load_csv_modal.ok.addEventListener(
104
+ 'click', () => FILE_MANAGER.loadCSVFile());
105
+ this.load_csv_modal.cancel.addEventListener(
106
+ 'click', () => DATASET_MANAGER.load_csv_modal.hide());
100
107
  this.conversion_modal = new ModalDialog('convert-modifiers');
101
108
  this.conversion_modal.ok.addEventListener(
102
109
  'click', () => DATASET_MANAGER.convertModifiers());
@@ -1149,4 +1156,127 @@ class GUIDatasetManager extends DatasetManager {
1149
1156
  this.updateDialog();
1150
1157
  }
1151
1158
 
1159
+ readCSVData(text) {
1160
+ // Parse text from uploaded file and create or overwrite datasets.
1161
+ const
1162
+ lines = text.trim().split(/\n/),
1163
+ n = lines.length;
1164
+ if(n < 2) {
1165
+ UI.warn('Data must have at least 2 lines: dataset names and default values');
1166
+ return false;
1167
+ }
1168
+ // Infer most likely delimiter.
1169
+ const
1170
+ tabs = text.split('\t').length - 1,
1171
+ tab0 = lines[0].split('\t').length - 1,
1172
+ tab1 = lines[1].split('\t').length - 1,
1173
+ semic0 = lines[0].split(';').length - 1,
1174
+ semics = text.split(';').length - 1 - semic0;
1175
+ let sep = '\t';
1176
+ if(!tabs || tab0 !== tab1) {
1177
+ // Tab is most likely NOT the separator.
1178
+ sep = (semics ? ';' : ',');
1179
+ }
1180
+ const
1181
+ parts = lines[0].split(sep),
1182
+ dsn = [];
1183
+ let quoted = false;
1184
+ for(let i = 0; i < parts.length; i++) {
1185
+ const
1186
+ p = parts[i],
1187
+ swq = /^\"(\"\")*($|[^\"])/.test(p),
1188
+ ewq = p.endsWith('"');
1189
+ if(!quoted && swq && !ewq) {
1190
+ dsn.push(p);
1191
+ quoted = true;
1192
+ } else if(quoted) {
1193
+ dsn[dsn.length - 1] += sep + p;
1194
+ quoted = !ewq;
1195
+ } else {
1196
+ dsn.push(p);
1197
+ }
1198
+ }
1199
+ for(let i = 0; i < dsn.length; i++) {
1200
+ const n = unquoteCSV(dsn[i].trim());
1201
+ if(!UI.validName(n)) {
1202
+ UI.warn(`Invalid dataset name "${n}" in column ${i}`);
1203
+ return false;
1204
+ }
1205
+ dsn[i] = n;
1206
+ }
1207
+ const
1208
+ ncol = dsn.length,
1209
+ dsa = [],
1210
+ dsdv = lines[1].split(sep);
1211
+ if(dsdv.length !== ncol) {
1212
+ UI.warn(`Number of default values (${dsdv.length}) does not match number of dataset names (${ncol})`);
1213
+ return false;
1214
+ }
1215
+ for(let i = 0; i < dsdv.length; i++) {
1216
+ const
1217
+ v = dsdv[i].trim(),
1218
+ sf = safeStrToFloat(v, NaN);
1219
+ if(isNaN(sf)) {
1220
+ UI.warn(`Invalid default value "${v}" in column ${i}`);
1221
+ return false;
1222
+ } else {
1223
+ dsa.push([sf]);
1224
+ }
1225
+ }
1226
+ for(let i = 2; i < n; i++) {
1227
+ const dsv = lines[i].trim().split(sep);
1228
+ if(dsv.length !== ncol) {
1229
+ UI.warn(`Number of values (${dsv.length}) on line ${i} does not match number of dataset names (${ncol})`);
1230
+ return false;
1231
+ }
1232
+ for(let j = 0; j < dsv.length; j++) {
1233
+ const
1234
+ v = dsv[j].trim(),
1235
+ sf = safeStrToFloat(v, '');
1236
+ if(sf === '' && v !== '') {
1237
+ UI.warn(`Invalid numerical value "${v}" for <strong>${dsn[j]}</strong> on line ${i}`);
1238
+ return false;
1239
+ } else {
1240
+ dsa[j].push(sf);
1241
+ }
1242
+ }
1243
+ }
1244
+ // Add or update datasets.
1245
+ let added = 0,
1246
+ updated = 0;
1247
+ for(let i = 0; i < dsn.length; i++) {
1248
+ const
1249
+ n = dsn[i],
1250
+ id = UI.nameToID(n),
1251
+ ods = MODEL.namedObjectByID(id),
1252
+ ds = ods || MODEL.addDataset(n);
1253
+ // NOTE: `ds` will now be either a new dataset or an existing one.
1254
+ if(ds) {
1255
+ // Keep track of added/updated datasets.
1256
+ const
1257
+ odv = ds.default_value,
1258
+ odata = ds.dataString;
1259
+ ds.default_value = safeStrToFloat(dsdv[i], 0);
1260
+ ds.data = dsa[i];
1261
+ if(ods) {
1262
+ if(ds.default_value !== odv || odata !== ds.dataString) updated++;
1263
+ } else {
1264
+ added++;
1265
+ }
1266
+ ds.computeStatistics();
1267
+ }
1268
+ }
1269
+ // Notify modeler of changes (if any).
1270
+ let msg = 'No datasets added or updated';
1271
+ if(added) {
1272
+ msg = pluralS(added, 'dataset') + ' added';
1273
+ if(updated) msg += ', ' + pluralS(updated, 'dataset') + ' updated';
1274
+ } else if(updated) {
1275
+ msg = pluralS(updated, 'dataset') + ' updated';
1276
+ }
1277
+ UI.notify(msg);
1278
+ this.updateDialog();
1279
+ return true;
1280
+ }
1281
+
1152
1282
  } // END of class GUIDatasetManager
@@ -391,6 +391,19 @@ class GUIFileManager {
391
391
  });
392
392
  }
393
393
 
394
+ loadCSVFile() {
395
+ document.getElementById('load-csv-modal').style.display = 'none';
396
+ try {
397
+ const file = document.getElementById('load-csv-file').files[0];
398
+ if(!file) return;
399
+ const reader = new FileReader();
400
+ reader.onload = (event) => DATASET_MANAGER.readCSVData(event.target.result);
401
+ reader.readAsText(file);
402
+ } catch(err) {
403
+ UI.alert('Error while reading file: ' + err);
404
+ }
405
+ }
406
+
394
407
  renderDiagramAsPNG(tight) {
395
408
  // When `tight` is TRUE, add no whitespace around the diagram.
396
409
  window.localStorage.removeItem('png-url');
@@ -225,6 +225,13 @@ function ellipsedText(text, n=50, m=10) {
225
225
  return text.slice(0, n) + ' \u2026 ' + text.slice(text.length - m);
226
226
  }
227
227
 
228
+ function unquoteCSV(s) {
229
+ // Returns a double-quoted string `s` without its quotes, and with
230
+ // quote pairs "" replaced by single " quotes.
231
+ if(!s.startsWith('"') || !s.endsWith('"')) return s;
232
+ return s.slice(1, -1).replaceAll('""', '"');
233
+ }
234
+
228
235
  //
229
236
  // Functions used when comparing two Linny-R models
230
237
  //
@@ -1132,6 +1139,7 @@ if(NODE) module.exports = {
1132
1139
  uniformDecimals: uniformDecimals,
1133
1140
  capitalized: capitalized,
1134
1141
  ellipsedText: ellipsedText,
1142
+ unquoteCSV: unquoteCSV,
1135
1143
  earlierVersion: earlierVersion,
1136
1144
  differences: differences,
1137
1145
  markFirstDifference: markFirstDifference,
@@ -444,6 +444,12 @@ class Expression {
444
444
  // expression).
445
445
  if(t < 0 || this.isStatic) t = 0;
446
446
  if(t >= v.length) return VM.UNDEFINED;
447
+ // Check for recursive calls.
448
+ if(v[t] === VM.COMPUTING) {
449
+ console.log('Already computing expression for', this.variableName);
450
+ console.log(this.text);
451
+ return VM.CYCLIC;
452
+ }
447
453
  // NOTES:
448
454
  // (1) When VM is setting up a tableau, values computed for the
449
455
  // look-ahead period must be recomputed.