linny-r 1.4.1 → 1.4.2

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": "1.4.1",
3
+ "version": "1.4.2",
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
@@ -165,8 +165,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
165
165
  // Inform user that newer version exists
166
166
  UI.check_update_modal.element('msg').innerHTML = [
167
167
  '<a href="', GITHUB_REPOSITORY,
168
- '/wiki/Linny-R-version-history#version-',
169
- info[1].replaceAll('.', ''), '" ',
168
+ '/releases/tag/v', info[1], '" ',
170
169
  'title="Click to view version release notes" ',
171
170
  'target="_blank">Version <strong>',
172
171
  info[1], '</strong></a> released on ',
@@ -1288,9 +1287,6 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; Btu">
1288
1287
  <div id="process-dlg" class="inp-dlg">
1289
1288
  <div class="dlg-title">
1290
1289
  Process properties
1291
- <div class="simbtn">
1292
- <img id="process-sim-btn" class="btn sim enab" src="images/process.png">
1293
- </div>
1294
1290
  <img class="cancel-btn" src="images/cancel.png">
1295
1291
  <img class="ok-btn" src="images/ok.png">
1296
1292
  </div>
@@ -346,20 +346,6 @@ div.contbtn {
346
346
  cursor: pointer;
347
347
  }
348
348
 
349
- div.simbtn {
350
- display: none;
351
- margin-left: 5px;
352
- }
353
-
354
- img.sim {
355
- height: 14px;
356
- width: 14px;
357
- }
358
-
359
- img.sim.enab:hover {
360
- background-color: #9e96e5;
361
- }
362
-
363
349
  input {
364
350
  vertical-align: baseline;
365
351
  }
@@ -302,14 +302,20 @@ class Controller {
302
302
  (name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
303
303
  }
304
304
 
305
- prefixesAndName(name) {
305
+ prefixesAndName(name, key=false) {
306
306
  // Returns name split exclusively at '[non-space]: [non-space]'
307
+ let sep = this.PREFIXER,
308
+ space = ' ';
309
+ if(key) {
310
+ sep = ':_';
311
+ space = '_';
312
+ }
307
313
  const
308
- s = name.split(this.PREFIXER),
314
+ s = name.split(sep),
309
315
  pan = [s[0]];
310
316
  for(let i = 1; i < s.length; i++) {
311
317
  const j = pan.length - 1;
312
- if(s[i].startsWith(' ') || (i > 0 && pan[j].endsWith(' '))) {
318
+ if(s[i].startsWith(space) || (i > 0 && pan[j].endsWith(space))) {
313
319
  pan[j] += s[i];
314
320
  } else {
315
321
  pan.push(s[i]);
@@ -350,14 +356,20 @@ class Controller {
350
356
  this.LINK_ARROW : this.CONSTRAINT_ARROW),
351
357
  nodes = name.split(arrow);
352
358
  for(let i = 0; i < nodes.length; i++) {
353
- nodes[i] = nodes[i].replace(/^:\s*/, prefix);
359
+ nodes[i] = nodes[i].replace(/^:\s*/, prefix)
360
+ // NOTE: An embedded double prefix, e.g., "xxx: : yyy" indicates
361
+ // that the second colon+space should be replaced by the prefix.
362
+ // This "double prefix" may occur only once in an entity name,
363
+ // hence no global regexp.
364
+ .replace(/(\w+):\s+:\s+(\w+)/, `$1: ${prefix}$2`);
354
365
  }
355
366
  return nodes.join(arrow);
356
367
  }
357
368
 
358
369
  tailNumber(name) {
359
370
  // Returns the string of digits at the end of `name`. If not there,
360
- // check prefixes (if any) from right to left for a tail number.
371
+ // check prefixes (if any) *from right to left* for a tail number.
372
+ // Thus, the number that is "closest" to the name part is returned.
361
373
  const pan = UI.prefixesAndName(name);
362
374
  let n = endsWithDigits(pan.pop());
363
375
  while(!n && pan.length > 0) {
@@ -366,6 +378,48 @@ class Controller {
366
378
  return n;
367
379
  }
368
380
 
381
+ compareFullNames(n1, n2, key=false) {
382
+ // Compare full names, considering prefixes in *left-to-right* order
383
+ // while taking into account the tailnumber for each part so that
384
+ // "xx: yy2: nnn" comes before "xx: yy10: nnn".
385
+ if(n1 === n2) return 0;
386
+ if(key) {
387
+ // NOTE: Replacing link and constraint arrows by two prefixers
388
+ // ensures that sort wil be first on FROM node, and then on TO node.
389
+ const p2 = UI.PREFIXER + UI.PREFIXER;
390
+ // Keys for links and constraints are not based on their names,
391
+ // so look up their names before comparing.
392
+ if(n1.indexOf('____') > 0 && MODEL.constraints[n1]) {
393
+ n1 = MODEL.constraints[n1].displayName
394
+ .replace(UI.CONSTRAINT_ARROW, p2);
395
+ } else if(n1.indexOf('___') > 0 && MODEL.links[n1]) {
396
+ n1 = MODEL.links[n1].displayName
397
+ .replace(UI.LINK_ARROW, p2);
398
+ }
399
+ if(n2.indexOf('____') > 0 && MODEL.constraints[n2]) {
400
+ n2 = MODEL.constraints[n2].displayName.
401
+ replace(UI.CONSTRAINT_ARROW, p2);
402
+ } else if(n2.indexOf('___') > 0 && MODEL.links[n2]) {
403
+ n2 = MODEL.links[n2].displayName
404
+ .replace(UI.LINK_ARROW, p2);
405
+ }
406
+ n1 = n1.toLowerCase().replaceAll(' ', '_');
407
+ n2 = n2.toLowerCase().replaceAll(' ', '_');
408
+ }
409
+ const
410
+ pan1 = UI.prefixesAndName(n1, key),
411
+ pan2 = UI.prefixesAndName(n2, key),
412
+ sl = Math.min(pan1.length, pan2.length);
413
+ let i = 0;
414
+ while(i < sl) {
415
+ const c = compareWithTailNumbers(pan1[i], pan2[i]);
416
+ if(c !== 0) return c;
417
+ i++;
418
+ }
419
+ return pan1.length - pan2.length;
420
+ }
421
+
422
+
369
423
  nameToID(name) {
370
424
  // Returns a name in lower case with link arrow replaced by three
371
425
  // underscores, constraint link arrow by four underscores, and spaces
@@ -7356,7 +7356,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7356
7356
  // is passed to differentiate between the DOM elements to be used
7357
7357
  const
7358
7358
  type = document.getElementById(prefix + 'variable-obj').value,
7359
- n_list = this.namesByType(VM.object_types[type]).sort(ciCompare),
7359
+ n_list = this.namesByType(VM.object_types[type]).sort(
7360
+ (a, b) => UI.compareFullNames(a, b)),
7360
7361
  vn = document.getElementById(prefix + 'variable-name'),
7361
7362
  options = [];
7362
7363
  // Add "empty" as first and initial option, but disable it.
@@ -7398,7 +7399,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7398
7399
  slist.push(d.modifiers[m].selector);
7399
7400
  }
7400
7401
  // Sort to present equations in alphabetical order
7401
- slist.sort(ciCompare);
7402
+ slist.sort((a, b) => UI.compareFullNames(a, b));
7402
7403
  for(let i = 0; i < slist.length; i++) {
7403
7404
  options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
7404
7405
  }
@@ -9900,13 +9901,7 @@ class GUIDatasetManager extends DatasetManager {
9900
9901
  dl = [],
9901
9902
  dnl = [],
9902
9903
  sd = this.selected_dataset,
9903
- ioclass = ['', 'import', 'export'],
9904
- ciPrefixCompare = (a, b) => {
9905
- const
9906
- pa = a.split(':_').join(' '),
9907
- pb = b.split(':_').join(' ');
9908
- return ciCompare(pa, pb);
9909
- };
9904
+ ioclass = ['', 'import', 'export'];
9910
9905
  for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
9911
9906
  // NOTE: do not list "black-boxed" entities
9912
9907
  !d.startsWith(UI.BLACK_BOX) &&
@@ -9917,7 +9912,7 @@ class GUIDatasetManager extends DatasetManager {
9917
9912
  dnl.push(d);
9918
9913
  }
9919
9914
  }
9920
- dnl.sort(ciPrefixCompare);
9915
+ dnl.sort((a, b) => UI.compareFullNames(a, b, true));
9921
9916
  // First determine indentation levels, prefixes and names
9922
9917
  const
9923
9918
  indent = [],
@@ -10678,7 +10673,7 @@ class GUIDatasetManager extends DatasetManager {
10678
10673
  const
10679
10674
  ln = document.getElementById('series-line-number'),
10680
10675
  lc = document.getElementById('series-line-count');
10681
- ln.innerHTML = this.series_data.value.substr(0,
10676
+ ln.innerHTML = this.series_data.value.substring(0,
10682
10677
  this.series_data.selectionStart).split('\n').length;
10683
10678
  lc.innerHTML = this.series_data.value.split('\n').length;
10684
10679
  }
@@ -12190,7 +12185,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
12190
12185
  const
12191
12186
  ds_dict = MODEL.listOfAllSelectors,
12192
12187
  html = [],
12193
- sl = Object.keys(ds_dict).sort(ciCompare);
12188
+ sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
12194
12189
  for(let i = 0; i < sl.length; i++) {
12195
12190
  const
12196
12191
  s = sl[i],
@@ -13175,7 +13170,7 @@ class GUIExperimentManager extends ExperimentManager {
13175
13170
  for(let i = 0; i < x.variables.length; i++) {
13176
13171
  addDistinct(x.variables[i].displayName, vl);
13177
13172
  }
13178
- vl.sort(ciCompare);
13173
+ vl.sort((a, b) => UI.compareFullNames(a, b));
13179
13174
  for(let i = 0; i < vl.length; i++) {
13180
13175
  ol.push(['<option value="', vl[i], '"',
13181
13176
  (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
@@ -15524,7 +15519,7 @@ class Finder {
15524
15519
  }
15525
15520
  }
15526
15521
  }
15527
- enl.sort(ciCompare);
15522
+ enl.sort((a, b) => UI.compareFullNames(a, b, true));
15528
15523
  }
15529
15524
  document.getElementById('finder-entity-imgs').innerHTML = imgs;
15530
15525
  let seid = 'etr';
@@ -15921,7 +15916,7 @@ class Finder {
15921
15916
  // No cost price calculation => trim associated attributes from header
15922
15917
  let p = ah.indexOf('\tCost price');
15923
15918
  if(p > 0) {
15924
- ah = ah.substr(0, p);
15919
+ ah = ah.substring(0, p);
15925
15920
  } else {
15926
15921
  // SOC is exogenous, and hence comes before F in header => replace
15927
15922
  ah = ah.replace('\tShare of cost', '');
@@ -16191,7 +16186,7 @@ class GUIReceiver {
16191
16186
  return response.text();
16192
16187
  })
16193
16188
  .then((data) => {
16194
- // For experiments, only display server response if warning or error
16189
+ // For experiments, only display server response if warning or error.
16195
16190
  UI.postResponseOK(data, !RECEIVER.experiment);
16196
16191
  // If execution completed, perform the call-back action if the
16197
16192
  // receiver is active (so not when auto-reporting a run).
@@ -2940,24 +2940,72 @@ class LinnyRModel {
2940
2940
  }
2941
2941
 
2942
2942
  get outputData() {
2943
- // Returns model results [data, statistics] in tab-separated format
2944
- // First create list of distinct variables used in charts
2945
- const vbls = [];
2943
+ // Returns model results [data, statistics] in tab-separated format.
2944
+ const
2945
+ vbls = [],
2946
+ names = [],
2947
+ scale_re = /\s+\(x[0-9\.\,]+\)$/;
2948
+ // First create list of distinct variables used in charts.
2949
+ // NOTE: Also include those that are not "visible" in a chart.
2946
2950
  for(let i = 0; i < this.charts.length; i++) {
2947
2951
  const c = this.charts[i];
2948
2952
  for(let j = 0; j < c.variables.length; j++) {
2949
- addDistinct(c.variables[j], vbls);
2953
+ let v = c.variables[j],
2954
+ vn = v.displayName;
2955
+ // If variable is scaled, do not include it as such, but include
2956
+ // a new unscaled chart variable.
2957
+ if(vn.match(scale_re)) {
2958
+ vn = vn.replace(scale_re, '');
2959
+ // Add only if (now unscaled) variable has not been added already.
2960
+ if(names.indexOf(vn) < 0) {
2961
+ // NOTE: Chart variable object is used ony as adummy, so NULL
2962
+ // can be used as its "owner chart".
2963
+ const cv = new ChartVariable(null);
2964
+ cv.setProperties(v.object, v.attribute, false, '#000000');
2965
+ vbls.push(cv);
2966
+ names.push(uvn);
2967
+ }
2968
+ } else if(names.indexOf(vn) < 0) {
2969
+ // Keep track of the dataset and dataset modifier variables,
2970
+ // so they will not be added in the next FOR loop.
2971
+ vbls.push(v);
2972
+ names.push(vn);
2973
+ }
2974
+ }
2975
+ }
2976
+ // Add new variables for each outcome dataset and each equation that
2977
+ // is not a chart variable.
2978
+ for(let id in this.datasets) if(this.datasets.hasOwnProperty(id)) {
2979
+ const
2980
+ ds = this.datasets[id],
2981
+ eq = (ds === this.equations_dataset);
2982
+ if(ds.outcome || eq) {
2983
+ for(let ms in ds.modifiers) if(ds.modifiers.hasOwnProperty(ms)) {
2984
+ const
2985
+ dm = ds.modifiers[ms],
2986
+ n = dm.displayName;
2987
+ // Do not add if already in the list.
2988
+ if(names.indexOf(n) < 0) {
2989
+ // Here, too, NULL can be used as "owner chart".
2990
+ const cv = new ChartVariable(null);
2991
+ cv.setProperties(ds, dm.selector, false, '#000000');
2992
+ vbls.push(cv);
2993
+ }
2994
+ }
2950
2995
  }
2951
2996
  }
2952
- // Create a new chart (without adding it to this model)
2997
+ // Sort variables by their name.
2998
+ vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
2999
+ // Create a new chart as dummy, so without adding it to this model.
2953
3000
  const c = new Chart();
2954
3001
  for(let i = 0; i < vbls.length; i++) {
2955
3002
  const v = vbls[i];
2956
3003
  c.addVariable(v.object.displayName, v.attribute);
2957
3004
  }
2958
- c.draw();
3005
+ // NOTE: Call `draw` with FALSE to prevent display in the chart manager.
3006
+ c.draw(false);
3007
+ // After drawing, all variables and their statistics have been computed.
2959
3008
  return [c.dataAsString, c.statisticsAsString];
2960
- // @@@TO DO: also add statistics on "outcome" datasets
2961
3009
  }
2962
3010
 
2963
3011
  get listOfAllSelectors() {
@@ -5278,14 +5326,14 @@ class NodeBox extends ObjectWithXYWH {
5278
5326
  MODEL.replaceEntityInExpressions(old_name, this.displayName);
5279
5327
  MODEL.inferIgnoredEntities();
5280
5328
  // NOTE: Renaming may affect the node's display size.
5281
- if(this.resize()) this.drawWithLinks();
5329
+ if(this.resize()) UI.drawSelection(MODEL);
5282
5330
  // NOTE: Only TRUE indicates a successful (cosmetic) name change.
5283
5331
  return true;
5284
5332
  }
5285
5333
 
5286
5334
  resize() {
5287
- // Resizes this node; returns TRUE iff size has changed
5288
- // So keep track of original width and height
5335
+ // Resizes this node; returns TRUE iff size has changed.
5336
+ // Therefore, keep track of original width and height.
5289
5337
  const
5290
5338
  ow = this.width,
5291
5339
  oh = this.height,
@@ -5341,12 +5389,13 @@ class NodeBox extends ObjectWithXYWH {
5341
5389
  }
5342
5390
 
5343
5391
  drawWithLinks() {
5344
- // TO DO: also draw relevant arrows when this is a cluster
5392
+ // TO DO: Also draw relevant arrows when this is a cluster.
5345
5393
  UI.drawObject(this);
5346
5394
  if(this instanceof Cluster) return;
5347
- // draw ALL arrows associated with this node
5395
+ // Draw ALL arrows associated with this node.
5348
5396
  const fc = this.cluster;
5349
- // make list of arrows that represent a link related to this node
5397
+ fc.categorizeEntities();
5398
+ // Make list of arrows that represent a link related to this node.
5350
5399
  let a,
5351
5400
  alist = [];
5352
5401
  for(let j = 0; j < fc.arrows.length; j++) {
@@ -5362,7 +5411,7 @@ class NodeBox extends ObjectWithXYWH {
5362
5411
  }
5363
5412
  }
5364
5413
  }
5365
- // draw all arrows in this list
5414
+ // Draw all arrows in this list.
5366
5415
  for(let i = 0; i < alist.length; i++) UI.drawObject(alist[i]);
5367
5416
  }
5368
5417
 
@@ -7323,7 +7372,7 @@ class Process extends Node {
7323
7372
  // without comments or their (X, Y) position
7324
7373
  n = MODEL.black_box_entities[id];
7325
7374
  // `n` is just the name, so remove the actor name if it was added
7326
- if(an) n = n.substr(0, n.lastIndexOf(an));
7375
+ if(an) n = n.substring(0, n.lastIndexOf(an));
7327
7376
  col = true;
7328
7377
  cmnts = '';
7329
7378
  x = 0;
@@ -8829,10 +8878,11 @@ class ChartVariable {
8829
8878
  this.non_zero_tally = 0;
8830
8879
  this.exceptions = 0;
8831
8880
  this.bin_tallies = [];
8881
+ this.wildcard_index = false;
8832
8882
  }
8833
8883
 
8834
8884
  setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true) {
8835
- // Sets the defining properties for this chart variable
8885
+ // Sets the defining properties for this chart variable.
8836
8886
  this.object = obj;
8837
8887
  this.attribute = attr;
8838
8888
  this.stacked = stck;
@@ -8844,12 +8894,18 @@ class ChartVariable {
8844
8894
 
8845
8895
  get displayName() {
8846
8896
  // Returns the name of the Linny-R entity and its attribute, followed
8847
- // by its scale factor unless it equals 1 (no scaling)
8897
+ // by its scale factor unless it equals 1 (no scaling).
8848
8898
  const sf = (this.scale_factor === 1 ? '' :
8849
8899
  ` (x${VM.sig4Dig(this.scale_factor)})`);
8850
- // NOTE: display name of equation is just the equations dataset modifier
8851
- if(this.object === MODEL.equations_dataset) return this.attribute + sf;
8852
- // NOTE: do not display vertical bar if no attribute is specified
8900
+ // NOTE: Display name of equation is just the equations dataset modifier.
8901
+ if(this.object === MODEL.equations_dataset) {
8902
+ let eqn = this.attribute;
8903
+ if(this.wildcard_index !== false) {
8904
+ eqn = eqn.replace('??', this.wildcard_index);
8905
+ }
8906
+ return eqn + sf;
8907
+ }
8908
+ // NOTE: Do not display the vertical bar if no attribute is specified.
8853
8909
  if(!this.attribute) return this.object.displayName + sf;
8854
8910
  return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
8855
8911
  }
@@ -8919,7 +8975,7 @@ class ChartVariable {
8919
8975
  }
8920
8976
 
8921
8977
  computeVector() {
8922
- // Compute vector for this variable (using run results if specified)
8978
+ // Compute vector for this variable (using run results if specified).
8923
8979
  let xrun = null,
8924
8980
  rr = null,
8925
8981
  ri = this.chart.run_index;
@@ -8934,10 +8990,10 @@ class ChartVariable {
8934
8990
  this.vector.length = 0;
8935
8991
  }
8936
8992
  }
8937
- // Compute vector and statistics only if vector is still empty
8993
+ // Compute vector and statistics only if vector is still empty.
8938
8994
  if(this.vector.length > 0) return;
8939
- // NOTE: expression vectors start at t = 0 with initial values that should
8940
- // not be included in statistics
8995
+ // NOTE: expression vectors start at t = 0 with initial values that
8996
+ // should not be included in statistics.
8941
8997
  let v,
8942
8998
  av = null,
8943
8999
  t_end;
@@ -8948,22 +9004,23 @@ class ChartVariable {
8948
9004
  this.non_zero_tally = 0;
8949
9005
  this.exceptions = 0;
8950
9006
  if(rr) {
8951
- // Use run results (time scaled) as "actual vector" `av` for this variable
9007
+ // Use run results (time scaled) as "actual vector" `av` for this
9008
+ // variable.
8952
9009
  const tsteps = Math.ceil(this.chart.time_horizon / this.chart.time_scale);
8953
9010
  av = [];
8954
- // NOTE: scaleData expects "pure" data, so slice off v[0]
9011
+ // NOTE: `scaleDataToVector` expects "pure" data, so slice off v[0].
8955
9012
  VM.scaleDataToVector(rr.vector.slice(1), av, xrun.time_step_duration,
8956
9013
  this.chart.time_scale, tsteps, 1);
8957
9014
  t_end = tsteps;
8958
9015
  } else {
8959
9016
  // Get the variable's own value (number, vector or expression)
8960
- // Special case: when an experiment is running, variables that depict a
8961
- // dataset with no explicit modifier must recompute the vector using the
8962
- // current experiment run combination
9017
+ // Special case: when an experiment is running, variables that
9018
+ // depict a dataset with no explicit modifier must recompute the
9019
+ // vector using the current experiment run combination.
8963
9020
  if(MODEL.running_experiment &&
8964
9021
  this.object instanceof Dataset && !this.attribute) {
8965
9022
  // Check if dataset modifiers match the combination of selectors
8966
- // for the active run
9023
+ // for the active run.
8967
9024
  const mm = this.object.matchingModifiers(
8968
9025
  MODEL.running_experiment.activeCombination);
8969
9026
  // If so, use the first (the list should contain at most 1 selector)
@@ -8982,21 +9039,24 @@ class ChartVariable {
8982
9039
  }
8983
9040
  t_end = MODEL.end_period - MODEL.start_period + 1;
8984
9041
  }
8985
- // NOTE: when a chart combines run results with dataset vectors, the latter
8986
- // may be longer than the # of time steps displayed in the chart
9042
+ // NOTE: when a chart combines run results with dataset vectors, the
9043
+ // latter may be longer than the # of time steps displayed in the chart.
8987
9044
  t_end = Math.min(t_end, this.chart.total_time_steps);
8988
9045
  this.N = t_end;
8989
9046
  for(let t = 0; t <= t_end; t++) {
8990
- // Get the result, store it, and incorporate it in statistics
9047
+ // Get the result, store it, and incorporate it in statistics.
8991
9048
  if(!av) {
8992
9049
  // Undefined attribute => zero (no error)
8993
9050
  v = 0;
8994
9051
  } else if(Array.isArray(av)) {
8995
- // Attribute value is a vector -- may be shorter than t => then use 0
9052
+ // Attribute value is a vector.
9053
+ // NOTE: This vector may be shorter than t; then use 0.
8996
9054
  v = (t < av.length ? av[t] : 0);
8997
9055
  } else if(av instanceof Expression) {
8998
- // Attribute value is an expression
8999
- v = av.result(t);
9056
+ // Attribute value is an expression. If this chart variable has
9057
+ // its wildcard vector index set, evaluate the expression with
9058
+ // this index as context number.
9059
+ v = av.result(t, this.wildcard_index);
9000
9060
  } else {
9001
9061
  // Attribute value must be a number
9002
9062
  v = av;
@@ -9165,7 +9225,7 @@ class Chart {
9165
9225
 
9166
9226
  variableIndexByName(n) {
9167
9227
  for(let i = 0; i < this.variables.length; i++) {
9168
- if(this.variables[i].displayName == n) return i;
9228
+ if(this.variables[i].displayName === n) return i;
9169
9229
  }
9170
9230
  return -1;
9171
9231
  }
@@ -9190,20 +9250,39 @@ class Chart {
9190
9250
  if(n === UI.EQUATIONS_DATASET_NAME) {
9191
9251
  // For equations only the attribute (modifier selector)
9192
9252
  dn = a;
9253
+ n = a;
9193
9254
  } else if(!a) {
9194
9255
  // If no attribute specified (=> dataset) only the entity name
9195
9256
  dn = n;
9196
9257
  }
9197
9258
  let vi = this.variableIndexByName(dn);
9198
9259
  if(vi >= 0) return vi;
9199
- // check whether name refers to a Linny-R entity defined by the model
9260
+ // Check whether name refers to a Linny-R entity defined by the model.
9200
9261
  let obj = MODEL.objectByName(n);
9201
9262
  if(obj === null) {
9202
9263
  UI.warn(`Unknown entity "${n}"`);
9203
9264
  return null;
9204
- } else {
9205
- // no attribute specified? then assume default
9265
+ }
9266
+ const eq = obj instanceof DatasetModifier;
9267
+ if(!eq) {
9268
+ // No equation and no attribute specified? Then assume default.
9206
9269
  if(a === '') a = obj.defaultAttribute;
9270
+ } else if(n.indexOf('??') >= 0) {
9271
+ // Special case: for wildcard equations, add dummy variables
9272
+ // for each vector in the wildcard vector set of the equation
9273
+ // expression.
9274
+ const
9275
+ vlist = [],
9276
+ clr = this.nextAvailableDefaultColor,
9277
+ indices = Object.keys(obj.expression.wildcard_vectors);
9278
+ for(let i = 0; i < indices.length; i++) {
9279
+ const v = new ChartVariable(this);
9280
+ v.setProperties(MODEL.equations_dataset, dn, false, clr);
9281
+ v.wildcard_index = parseInt(indices[i]);
9282
+ this.variables.push(v);
9283
+ vlist.push(v);
9284
+ }
9285
+ return vlist;
9207
9286
  }
9208
9287
  const v = new ChartVariable(this);
9209
9288
  v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
@@ -9296,7 +9375,7 @@ class Chart {
9296
9375
  return VM.sig2Dig(s / 8760) + 'y';
9297
9376
  }
9298
9377
 
9299
- draw() {
9378
+ draw(display=true) {
9300
9379
  // NOTE: The SVG drawing area is fixed to be 500 pixels high, so that
9301
9380
  // when saved as a file, it will (at 300 dpi) be about 2 inches high.
9302
9381
  // Its width will equal its hight times the W/H-ratio of the chart
@@ -9471,6 +9550,10 @@ class Chart {
9471
9550
  }
9472
9551
  }
9473
9552
 
9553
+ // Now all vectors have been computed. If `display` is FALSE, this
9554
+ // indicates that data is used only to save model results.
9555
+ if(!display) return;
9556
+
9474
9557
  // Define the bins when drawing as histogram
9475
9558
  if(this.histogram) {
9476
9559
  this.value_range = maxv - minv;
@@ -124,9 +124,9 @@ function msecToTime(msec) {
124
124
  const ts = new Date(msec).toISOString().slice(11, -1).split('.');
125
125
  let hms = ts[0], ms = ts[1];
126
126
  // Trim zero hours and minutes
127
- while(hms.startsWith('00:')) hms = hms.substr(3);
127
+ while(hms.startsWith('00:')) hms = hms.substring(3);
128
128
  // Trim leading zero on first number
129
- if(hms.startsWith('00')) hms = hms.substr(1);
129
+ if(hms.startsWith('00')) hms = hms.substring(1);
130
130
  // Trim msec when minutes > 0
131
131
  if(hms.indexOf(':') > 0) return hms;
132
132
  // If < 1 second, return as milliseconds
@@ -173,7 +173,7 @@ function uniformDecimals(data) {
173
173
  ss = v.split('e');
174
174
  x = ss[1];
175
175
  if(x.length < maxe) {
176
- x = x[0] + '0' + x.substr(1);
176
+ x = x[0] + '0' + x.substring(1);
177
177
  }
178
178
  data[i] = ss[0] + 'e' + x;
179
179
  } else if(maxi > 3) {
@@ -478,6 +478,21 @@ function matchingNumber(m, s) {
478
478
  return (n == m ? n : false);
479
479
  }
480
480
 
481
+ function compareWithTailNumbers(s1, s2) {
482
+ // Returns 0 on equal, an integer < 0 if `s1` comes before `s2`, and
483
+ // an integer > 0 if `s2` comes before `s1`.
484
+ if(s1 === s2) return 0;
485
+ let tn1 = endsWithDigits(s1),
486
+ tn2 = endsWithDigits(s2);
487
+ if(tn1) s1 = s1.slice(0, -tn1.length);
488
+ if(tn2) s2 = s2.slice(0, -tn2.length);
489
+ let c = ciCompare(s1, s2);
490
+ if(c !== 0 || !(tn1 || tn2)) return c;
491
+ if(tn1 && tn2) return parseInt(tn1) - parseInt(tn2);
492
+ if(tn2) return -1;
493
+ return 1;
494
+ }
495
+
481
496
  function compareSelectors(s1, s2) {
482
497
  // Dataset selectors comparison is case-insensitive, and puts wildcards
483
498
  // last, where * comes later than ?
@@ -492,7 +507,7 @@ function compareSelectors(s1, s2) {
492
507
  star2 = s2.indexOf('*');
493
508
  if(star1 >= 0) {
494
509
  if(star2 < 0) return 1;
495
- return s1.localeCompare(s2);
510
+ return ciCompare(s1, s2);
496
511
  }
497
512
  if(star2 >= 0) return -1;
498
513
  // Replace ? by | because | has a higher ASCII value than all other chars
@@ -525,7 +540,7 @@ function compareSelectors(s1, s2) {
525
540
  while(i >= 0 && s_1[i] === '-') i--;
526
541
  // If trailing minuses, replace by as many spaces and add an exclamation point
527
542
  if(i < n - 1) {
528
- s_1 = s_1.substr(0, i);
543
+ s_1 = s_1.substring(0, i);
529
544
  while(s_1.length < n) s_1 += ' ';
530
545
  s_1 += '!';
531
546
  }
@@ -534,7 +549,7 @@ function compareSelectors(s1, s2) {
534
549
  i = n - 1;
535
550
  while(i >= 0 && s_2[i] === '-') i--;
536
551
  if(i < n - 1) {
537
- s_2 = s_2.substr(0, i);
552
+ s_2 = s_2.substring(0, i);
538
553
  while(s_2.length < n) s_2 += ' ';
539
554
  s_2 += '!';
540
555
  }
@@ -814,8 +829,9 @@ function stringToFloatArray(s) {
814
829
  a = [];
815
830
  while(i <= s.length) {
816
831
  const
817
- h = s.substr(i - 8, 8),
818
- r = h.substr(6, 2) + h.substr(4, 2) + h.substr(2, 2) + h.substr(0, 2);
832
+ h = s.substring(i - 8, i),
833
+ r = h.substring(6, 2) + h.substring(4, 2) +
834
+ h.substring(2, 2) + h.substring(0, 2);
819
835
  a.push(hexToFloat(r));
820
836
  i += 8;
821
837
  }
@@ -830,7 +846,7 @@ function hexToBytes(hex) {
830
846
  // Converts a hex string to a Uint8Array
831
847
  const bytes = [];
832
848
  for(let i = 0; i < hex.length; i += 2) {
833
- bytes.push(parseInt(hex.substr(i, 2), 16));
849
+ bytes.push(parseInt(hex.substring(i, i + 2), 16));
834
850
  }
835
851
  return new Uint8Array(bytes);
836
852
  }
@@ -713,14 +713,14 @@ class ExpressionParser {
713
713
  // t (current time step, this is the default),
714
714
  if('#cfijklnprst'.includes(offs[0].charAt(0))) {
715
715
  anchor1 = offs[0].charAt(0);
716
- offset1 = safeStrToInt(offs[0].substr(1));
716
+ offset1 = safeStrToInt(offs[0].substring(1));
717
717
  } else {
718
718
  offset1 = safeStrToInt(offs[0]);
719
719
  }
720
720
  if(offs.length > 1) {
721
721
  if('#cfijklnprst'.includes(offs[1].charAt(0))) {
722
722
  anchor2 = offs[1].charAt(0);
723
- offset2 = safeStrToInt(offs[1].substr(1));
723
+ offset2 = safeStrToInt(offs[1].substring(1));
724
724
  } else {
725
725
  offset2 = safeStrToInt(offs[1]);
726
726
  }
@@ -757,7 +757,7 @@ class ExpressionParser {
757
757
  };
758
758
  // NOTE: name should then be in the experiment's variable list
759
759
  name = s[1].trim();
760
- s = s[0].substr(1);
760
+ s = s[0].substring(1);
761
761
  // Check for scaling method
762
762
  // NOTE: simply ignore $ unless it indicates a valid method
763
763
  const msep = s.indexOf('$');
@@ -967,6 +967,10 @@ class ExpressionParser {
967
967
  return false;
968
968
  }
969
969
  }
970
+ /*
971
+ // DEPRECATED -- Modeler can deal with this by smartly using AND
972
+ // clauses like "&x: &y:" to limit set to specific prefixes.
973
+
970
974
  // Deal with "prefix inheritance" when pattern starts with a colon.
971
975
  if(pat.startsWith(':') && this.owner_prefix) {
972
976
  // Add a "must start with" AND condition to all OR clauses of the
@@ -979,6 +983,7 @@ class ExpressionParser {
979
983
  }
980
984
  pat = oc.join('|');
981
985
  }
986
+ */
982
987
  // NOTE: For patterns, assume that # *always* denotes the context-
983
988
  // sensitive number #, because if modelers wishes to include
984
989
  // ANY number, they can make their pattern less selective.
@@ -1449,7 +1454,7 @@ class ExpressionParser {
1449
1454
  this.los = 1;
1450
1455
  this.error = 'Missing closing bracket \']\'';
1451
1456
  } else {
1452
- v = this.expr.substr(this.pit + 1, i - 1 - this.pit);
1457
+ v = this.expr.substring(this.pit + 1, i);
1453
1458
  this.pit = i + 1;
1454
1459
  // NOTE: Enclosing brackets are also part of this symbol
1455
1460
  this.los = v.length + 2;
@@ -1467,7 +1472,7 @@ class ExpressionParser {
1467
1472
  this.los = 1;
1468
1473
  this.error = 'Unmatched quote';
1469
1474
  } else {
1470
- v = this.expr.substr(this.pit + 1, i - 1 - this.pit);
1475
+ v = this.expr.substring(this.pit + 1, i);
1471
1476
  this.pit = i + 1;
1472
1477
  // NOTE: Enclosing quotes are also part of this symbol
1473
1478
  this.los = v.length + 2;
@@ -1520,7 +1525,7 @@ class ExpressionParser {
1520
1525
  this.los++;
1521
1526
  }
1522
1527
  // ... but trim spaces from the symbol
1523
- v = this.expr.substr(this.pit, this.los).trim();
1528
+ v = this.expr.substring(this.pit, this.pit + this.los).trim();
1524
1529
  // Ignore case
1525
1530
  l = v.toLowerCase();
1526
1531
  if(l === '#') {
@@ -5929,7 +5934,9 @@ function VMI_push_dataset_modifier(x, args) {
5929
5934
  // NOTE: Use the "local" time step for expression x, i.e., the top
5930
5935
  // value of the expression's time step stack `x.step`.
5931
5936
  tot = twoOffsetTimeStep(x.step[x.step.length - 1],
5932
- args[1], args[2], args[3], args[4], 1, x);
5937
+ args[1], args[2], args[3], args[4], 1, x),
5938
+ // Record whether either anchor uses the context-sensitive number.
5939
+ hashtag_index = (args[1] === '#' || args[3] === '#');
5933
5940
  // NOTE: Sanity check to facilitate debugging; if no dataset is provided,
5934
5941
  // the script will still break at the LET statement below.
5935
5942
  if(!ds) console.log('ERROR: VMI_push_dataset_modifier without dataset',
@@ -5947,7 +5954,7 @@ function VMI_push_dataset_modifier(x, args) {
5947
5954
  t = t % obj.length;
5948
5955
  if(t < 0) t += obj.length;
5949
5956
  }
5950
- if(args[1] === '#' || args[3] === '#') {
5957
+ if(hashtag_index) {
5951
5958
  // NOTE: Add 1 because (parent) anchors are 1-based.
5952
5959
  ds.parent_anchor = t + 1;
5953
5960
  if(DEBUGGING) {
@@ -5991,12 +5998,21 @@ function VMI_push_dataset_modifier(x, args) {
5991
5998
  if(t >= 0 && t < obj.length) {
5992
5999
  v = obj[t];
5993
6000
  } else if(ds.array && t >= obj.length) {
5994
- // Set error value if array index is out of bounds.
5995
- v = VM.ARRAY_INDEX;
5996
- VM.out_of_bounds_array = ds.displayName;
5997
- VM.out_of_bounds_msg = `Index ${VM.sig2Dig(t + 1)} not in array dataset ` +
5998
- `${ds.displayName}, which has length ${obj.length}`;
5999
- console.log(VM.out_of_bounds_msg);
6001
+ // Ensure that value of t is human-readable.
6002
+ // NOTE: Add 1 to compensate for earlier t-- to make `t` zero-based.
6003
+ const index = VM.sig2Dig(t + 1);
6004
+ // Special case: index is undefined because # was undefined.
6005
+ if(hashtag_index && index === '\u2047') {
6006
+ // In such cases, return the default value of the dataset.
6007
+ v = ds.default_value;
6008
+ } else {
6009
+ // Set error value to indicate that array index is out of bounds.
6010
+ v = VM.ARRAY_INDEX;
6011
+ VM.out_of_bounds_array = ds.displayName;
6012
+ VM.out_of_bounds_msg = `Index ${index} not in array dataset ` +
6013
+ `${ds.displayName}, which has length ${obj.length}`;
6014
+ console.log(VM.out_of_bounds_msg);
6015
+ }
6000
6016
  }
6001
6017
  // Fall through: no change to `v` => dataset default value is pushed.
6002
6018
  } else {