linny-r 1.4.1 → 1.4.3

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.
@@ -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
 
@@ -7290,6 +7339,7 @@ class Process extends Node {
7290
7339
  a.LB = this.lower_bound.asAttribute;
7291
7340
  a.UB = (this.equal_bounds ? a.LB : this.upper_bound.asAttribute);
7292
7341
  a.IL = this.initial_level.asAttribute;
7342
+ a.LCF = this.pace_expression.asAttribute;
7293
7343
  if(MODEL.solved) {
7294
7344
  const t = MODEL.t;
7295
7345
  a.L = this.level[t];
@@ -7323,7 +7373,7 @@ class Process extends Node {
7323
7373
  // without comments or their (X, Y) position
7324
7374
  n = MODEL.black_box_entities[id];
7325
7375
  // `n` is just the name, so remove the actor name if it was added
7326
- if(an) n = n.substr(0, n.lastIndexOf(an));
7376
+ if(an) n = n.substring(0, n.lastIndexOf(an));
7327
7377
  col = true;
7328
7378
  cmnts = '';
7329
7379
  x = 0;
@@ -7551,6 +7601,8 @@ class Product extends Node {
7551
7601
  if(MODEL.infer_cost_prices) {
7552
7602
  a.CP = this.cost_price[t];
7553
7603
  a.HCP = this.highest_cost_price[t];
7604
+ // Highest cost price may be undefined if product has no inflows.
7605
+ if(a.HCP === VM.MINUS_INFINITY) a.HCP = '';
7554
7606
  }
7555
7607
  }
7556
7608
  return a;
@@ -8829,10 +8881,11 @@ class ChartVariable {
8829
8881
  this.non_zero_tally = 0;
8830
8882
  this.exceptions = 0;
8831
8883
  this.bin_tallies = [];
8884
+ this.wildcard_index = false;
8832
8885
  }
8833
8886
 
8834
8887
  setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true) {
8835
- // Sets the defining properties for this chart variable
8888
+ // Sets the defining properties for this chart variable.
8836
8889
  this.object = obj;
8837
8890
  this.attribute = attr;
8838
8891
  this.stacked = stck;
@@ -8844,12 +8897,18 @@ class ChartVariable {
8844
8897
 
8845
8898
  get displayName() {
8846
8899
  // Returns the name of the Linny-R entity and its attribute, followed
8847
- // by its scale factor unless it equals 1 (no scaling)
8900
+ // by its scale factor unless it equals 1 (no scaling).
8848
8901
  const sf = (this.scale_factor === 1 ? '' :
8849
8902
  ` (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
8903
+ // NOTE: Display name of equation is just the equations dataset modifier.
8904
+ if(this.object === MODEL.equations_dataset) {
8905
+ let eqn = this.attribute;
8906
+ if(this.wildcard_index !== false) {
8907
+ eqn = eqn.replace('??', this.wildcard_index);
8908
+ }
8909
+ return eqn + sf;
8910
+ }
8911
+ // NOTE: Do not display the vertical bar if no attribute is specified.
8853
8912
  if(!this.attribute) return this.object.displayName + sf;
8854
8913
  return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
8855
8914
  }
@@ -8919,7 +8978,7 @@ class ChartVariable {
8919
8978
  }
8920
8979
 
8921
8980
  computeVector() {
8922
- // Compute vector for this variable (using run results if specified)
8981
+ // Compute vector for this variable (using run results if specified).
8923
8982
  let xrun = null,
8924
8983
  rr = null,
8925
8984
  ri = this.chart.run_index;
@@ -8934,10 +8993,10 @@ class ChartVariable {
8934
8993
  this.vector.length = 0;
8935
8994
  }
8936
8995
  }
8937
- // Compute vector and statistics only if vector is still empty
8996
+ // Compute vector and statistics only if vector is still empty.
8938
8997
  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
8998
+ // NOTE: expression vectors start at t = 0 with initial values that
8999
+ // should not be included in statistics.
8941
9000
  let v,
8942
9001
  av = null,
8943
9002
  t_end;
@@ -8948,22 +9007,23 @@ class ChartVariable {
8948
9007
  this.non_zero_tally = 0;
8949
9008
  this.exceptions = 0;
8950
9009
  if(rr) {
8951
- // Use run results (time scaled) as "actual vector" `av` for this variable
9010
+ // Use run results (time scaled) as "actual vector" `av` for this
9011
+ // variable.
8952
9012
  const tsteps = Math.ceil(this.chart.time_horizon / this.chart.time_scale);
8953
9013
  av = [];
8954
- // NOTE: scaleData expects "pure" data, so slice off v[0]
9014
+ // NOTE: `scaleDataToVector` expects "pure" data, so slice off v[0].
8955
9015
  VM.scaleDataToVector(rr.vector.slice(1), av, xrun.time_step_duration,
8956
9016
  this.chart.time_scale, tsteps, 1);
8957
9017
  t_end = tsteps;
8958
9018
  } else {
8959
9019
  // 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
9020
+ // Special case: when an experiment is running, variables that
9021
+ // depict a dataset with no explicit modifier must recompute the
9022
+ // vector using the current experiment run combination.
8963
9023
  if(MODEL.running_experiment &&
8964
9024
  this.object instanceof Dataset && !this.attribute) {
8965
9025
  // Check if dataset modifiers match the combination of selectors
8966
- // for the active run
9026
+ // for the active run.
8967
9027
  const mm = this.object.matchingModifiers(
8968
9028
  MODEL.running_experiment.activeCombination);
8969
9029
  // If so, use the first (the list should contain at most 1 selector)
@@ -8982,21 +9042,24 @@ class ChartVariable {
8982
9042
  }
8983
9043
  t_end = MODEL.end_period - MODEL.start_period + 1;
8984
9044
  }
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
9045
+ // NOTE: when a chart combines run results with dataset vectors, the
9046
+ // latter may be longer than the # of time steps displayed in the chart.
8987
9047
  t_end = Math.min(t_end, this.chart.total_time_steps);
8988
9048
  this.N = t_end;
8989
9049
  for(let t = 0; t <= t_end; t++) {
8990
- // Get the result, store it, and incorporate it in statistics
9050
+ // Get the result, store it, and incorporate it in statistics.
8991
9051
  if(!av) {
8992
9052
  // Undefined attribute => zero (no error)
8993
9053
  v = 0;
8994
9054
  } else if(Array.isArray(av)) {
8995
- // Attribute value is a vector -- may be shorter than t => then use 0
9055
+ // Attribute value is a vector.
9056
+ // NOTE: This vector may be shorter than t; then use 0.
8996
9057
  v = (t < av.length ? av[t] : 0);
8997
9058
  } else if(av instanceof Expression) {
8998
- // Attribute value is an expression
8999
- v = av.result(t);
9059
+ // Attribute value is an expression. If this chart variable has
9060
+ // its wildcard vector index set, evaluate the expression with
9061
+ // this index as context number.
9062
+ v = av.result(t, this.wildcard_index);
9000
9063
  } else {
9001
9064
  // Attribute value must be a number
9002
9065
  v = av;
@@ -9165,7 +9228,7 @@ class Chart {
9165
9228
 
9166
9229
  variableIndexByName(n) {
9167
9230
  for(let i = 0; i < this.variables.length; i++) {
9168
- if(this.variables[i].displayName == n) return i;
9231
+ if(this.variables[i].displayName === n) return i;
9169
9232
  }
9170
9233
  return -1;
9171
9234
  }
@@ -9190,20 +9253,39 @@ class Chart {
9190
9253
  if(n === UI.EQUATIONS_DATASET_NAME) {
9191
9254
  // For equations only the attribute (modifier selector)
9192
9255
  dn = a;
9256
+ n = a;
9193
9257
  } else if(!a) {
9194
9258
  // If no attribute specified (=> dataset) only the entity name
9195
9259
  dn = n;
9196
9260
  }
9197
9261
  let vi = this.variableIndexByName(dn);
9198
9262
  if(vi >= 0) return vi;
9199
- // check whether name refers to a Linny-R entity defined by the model
9263
+ // Check whether name refers to a Linny-R entity defined by the model.
9200
9264
  let obj = MODEL.objectByName(n);
9201
9265
  if(obj === null) {
9202
9266
  UI.warn(`Unknown entity "${n}"`);
9203
9267
  return null;
9204
- } else {
9205
- // no attribute specified? then assume default
9268
+ }
9269
+ const eq = obj instanceof DatasetModifier;
9270
+ if(!eq) {
9271
+ // No equation and no attribute specified? Then assume default.
9206
9272
  if(a === '') a = obj.defaultAttribute;
9273
+ } else if(n.indexOf('??') >= 0) {
9274
+ // Special case: for wildcard equations, add dummy variables
9275
+ // for each vector in the wildcard vector set of the equation
9276
+ // expression.
9277
+ const
9278
+ vlist = [],
9279
+ clr = this.nextAvailableDefaultColor,
9280
+ indices = Object.keys(obj.expression.wildcard_vectors);
9281
+ for(let i = 0; i < indices.length; i++) {
9282
+ const v = new ChartVariable(this);
9283
+ v.setProperties(MODEL.equations_dataset, dn, false, clr);
9284
+ v.wildcard_index = parseInt(indices[i]);
9285
+ this.variables.push(v);
9286
+ vlist.push(v);
9287
+ }
9288
+ return vlist;
9207
9289
  }
9208
9290
  const v = new ChartVariable(this);
9209
9291
  v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
@@ -9296,7 +9378,7 @@ class Chart {
9296
9378
  return VM.sig2Dig(s / 8760) + 'y';
9297
9379
  }
9298
9380
 
9299
- draw() {
9381
+ draw(display=true) {
9300
9382
  // NOTE: The SVG drawing area is fixed to be 500 pixels high, so that
9301
9383
  // when saved as a file, it will (at 300 dpi) be about 2 inches high.
9302
9384
  // Its width will equal its hight times the W/H-ratio of the chart
@@ -9471,6 +9553,10 @@ class Chart {
9471
9553
  }
9472
9554
  }
9473
9555
 
9556
+ // Now all vectors have been computed. If `display` is FALSE, this
9557
+ // indicates that data is used only to save model results.
9558
+ if(!display) return;
9559
+
9474
9560
  // Define the bins when drawing as histogram
9475
9561
  if(this.histogram) {
9476
9562
  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) {
@@ -186,9 +186,14 @@ function uniformDecimals(data) {
186
186
  }
187
187
  }
188
188
 
189
+ function capitalized(s) {
190
+ // Returns string `s` with its first letter capitalized.
191
+ return s.charAt(0).toUpperCase() + s.slice(1);
192
+ }
193
+
189
194
  function ellipsedText(text, n=50, m=10) {
190
- // Returns `text` with ellipsis " ... " between its first `n` and last `m`
191
- // characters
195
+ // Returns `text` with ellipsis " ... " between its first `n` and
196
+ // last `m` characters.
192
197
  if(text.length <= n + m + 3) return text;
193
198
  return text.slice(0, n) + ' \u2026 ' + text.slice(text.length - m);
194
199
  }
@@ -478,6 +483,21 @@ function matchingNumber(m, s) {
478
483
  return (n == m ? n : false);
479
484
  }
480
485
 
486
+ function compareWithTailNumbers(s1, s2) {
487
+ // Returns 0 on equal, an integer < 0 if `s1` comes before `s2`, and
488
+ // an integer > 0 if `s2` comes before `s1`.
489
+ if(s1 === s2) return 0;
490
+ let tn1 = endsWithDigits(s1),
491
+ tn2 = endsWithDigits(s2);
492
+ if(tn1) s1 = s1.slice(0, -tn1.length);
493
+ if(tn2) s2 = s2.slice(0, -tn2.length);
494
+ let c = ciCompare(s1, s2);
495
+ if(c !== 0 || !(tn1 || tn2)) return c;
496
+ if(tn1 && tn2) return parseInt(tn1) - parseInt(tn2);
497
+ if(tn2) return -1;
498
+ return 1;
499
+ }
500
+
481
501
  function compareSelectors(s1, s2) {
482
502
  // Dataset selectors comparison is case-insensitive, and puts wildcards
483
503
  // last, where * comes later than ?
@@ -492,7 +512,7 @@ function compareSelectors(s1, s2) {
492
512
  star2 = s2.indexOf('*');
493
513
  if(star1 >= 0) {
494
514
  if(star2 < 0) return 1;
495
- return s1.localeCompare(s2);
515
+ return ciCompare(s1, s2);
496
516
  }
497
517
  if(star2 >= 0) return -1;
498
518
  // Replace ? by | because | has a higher ASCII value than all other chars
@@ -525,7 +545,7 @@ function compareSelectors(s1, s2) {
525
545
  while(i >= 0 && s_1[i] === '-') i--;
526
546
  // If trailing minuses, replace by as many spaces and add an exclamation point
527
547
  if(i < n - 1) {
528
- s_1 = s_1.substr(0, i);
548
+ s_1 = s_1.substring(0, i);
529
549
  while(s_1.length < n) s_1 += ' ';
530
550
  s_1 += '!';
531
551
  }
@@ -534,7 +554,7 @@ function compareSelectors(s1, s2) {
534
554
  i = n - 1;
535
555
  while(i >= 0 && s_2[i] === '-') i--;
536
556
  if(i < n - 1) {
537
- s_2 = s_2.substr(0, i);
557
+ s_2 = s_2.substring(0, i);
538
558
  while(s_2.length < n) s_2 += ' ';
539
559
  s_2 += '!';
540
560
  }
@@ -814,8 +834,9 @@ function stringToFloatArray(s) {
814
834
  a = [];
815
835
  while(i <= s.length) {
816
836
  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);
837
+ h = s.substring(i - 8, i),
838
+ r = h.substring(6, 2) + h.substring(4, 2) +
839
+ h.substring(2, 2) + h.substring(0, 2);
819
840
  a.push(hexToFloat(r));
820
841
  i += 8;
821
842
  }
@@ -830,7 +851,7 @@ function hexToBytes(hex) {
830
851
  // Converts a hex string to a Uint8Array
831
852
  const bytes = [];
832
853
  for(let i = 0; i < hex.length; i += 2) {
833
- bytes.push(parseInt(hex.substr(i, 2), 16));
854
+ bytes.push(parseInt(hex.substring(i, i + 2), 16));
834
855
  }
835
856
  return new Uint8Array(bytes);
836
857
  }
@@ -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 === '#') {
@@ -2065,6 +2070,16 @@ class VirtualMachine {
2065
2070
  P: this.process_attr,
2066
2071
  Q: this.product_attr
2067
2072
  };
2073
+ this.entity_attribute_names = {};
2074
+ for(let i = 0; i < this.entity_letters.length; i++) {
2075
+ const
2076
+ el = this.entity_letters.charAt(i),
2077
+ ac = this.attribute_codes[el];
2078
+ this.entity_attribute_names[el] = [];
2079
+ for(let j = 0; j < ac.length; j++) {
2080
+ this.entity_attribute_names[el].push(ac[j]);
2081
+ }
2082
+ }
2068
2083
  // Level-based attributes are computed only AFTER optimization
2069
2084
  this.level_based_attr = ['L', 'CP', 'HCP', 'CF', 'CI', 'CO', 'F', 'A'];
2070
2085
  this.object_types = ['Process', 'Product', 'Cluster', 'Link', 'Constraint',
@@ -5929,7 +5944,9 @@ function VMI_push_dataset_modifier(x, args) {
5929
5944
  // NOTE: Use the "local" time step for expression x, i.e., the top
5930
5945
  // value of the expression's time step stack `x.step`.
5931
5946
  tot = twoOffsetTimeStep(x.step[x.step.length - 1],
5932
- args[1], args[2], args[3], args[4], 1, x);
5947
+ args[1], args[2], args[3], args[4], 1, x),
5948
+ // Record whether either anchor uses the context-sensitive number.
5949
+ hashtag_index = (args[1] === '#' || args[3] === '#');
5933
5950
  // NOTE: Sanity check to facilitate debugging; if no dataset is provided,
5934
5951
  // the script will still break at the LET statement below.
5935
5952
  if(!ds) console.log('ERROR: VMI_push_dataset_modifier without dataset',
@@ -5947,7 +5964,7 @@ function VMI_push_dataset_modifier(x, args) {
5947
5964
  t = t % obj.length;
5948
5965
  if(t < 0) t += obj.length;
5949
5966
  }
5950
- if(args[1] === '#' || args[3] === '#') {
5967
+ if(hashtag_index) {
5951
5968
  // NOTE: Add 1 because (parent) anchors are 1-based.
5952
5969
  ds.parent_anchor = t + 1;
5953
5970
  if(DEBUGGING) {
@@ -5991,12 +6008,21 @@ function VMI_push_dataset_modifier(x, args) {
5991
6008
  if(t >= 0 && t < obj.length) {
5992
6009
  v = obj[t];
5993
6010
  } 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);
6011
+ // Ensure that value of t is human-readable.
6012
+ // NOTE: Add 1 to compensate for earlier t-- to make `t` zero-based.
6013
+ const index = VM.sig2Dig(t + 1);
6014
+ // Special case: index is undefined because # was undefined.
6015
+ if(hashtag_index && index === '\u2047') {
6016
+ // In such cases, return the default value of the dataset.
6017
+ v = ds.default_value;
6018
+ } else {
6019
+ // Set error value to indicate that array index is out of bounds.
6020
+ v = VM.ARRAY_INDEX;
6021
+ VM.out_of_bounds_array = ds.displayName;
6022
+ VM.out_of_bounds_msg = `Index ${index} not in array dataset ` +
6023
+ `${ds.displayName}, which has length ${obj.length}`;
6024
+ console.log(VM.out_of_bounds_msg);
6025
+ }
6000
6026
  }
6001
6027
  // Fall through: no change to `v` => dataset default value is pushed.
6002
6028
  } else {