linny-r 2.1.5 → 2.1.7

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.
@@ -542,7 +542,7 @@ class LinnyRModel {
542
542
  e = this.objectByName(en);
543
543
  if(!e) return `Unknown model entity "${en}"`;
544
544
  const
545
- ao = ea[1].split('@'),
545
+ ao = (ea.length > 1 ? ea[1].split('@') : ['']),
546
546
  a = ao[0].trim();
547
547
  // Valid if no attribute, as all entity types have a default attribute.
548
548
  if(!a) return true;
@@ -1636,8 +1636,7 @@ class LinnyRModel {
1636
1636
  const ci = this.indexOfChart(title);
1637
1637
  if(ci >= 0) return this.charts[ci];
1638
1638
  // Otherwise, add it. NOTE: Unlike datasets, charts are not "entities".
1639
- let c = new Chart();
1640
- c.title = title;
1639
+ let c = new Chart(title);
1641
1640
  if(node) c.initFromXML(node);
1642
1641
  this.charts.push(c);
1643
1642
  // Sort the chart titles alphabetically...
@@ -3323,7 +3322,7 @@ class LinnyRModel {
3323
3322
  vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
3324
3323
  // Create a new chart as dummy, so without adding it to this model.
3325
3324
  const
3326
- c = new Chart(),
3325
+ c = new Chart('__d_u_m_m_y__c_h_a_r_t__'),
3327
3326
  wcdm = [];
3328
3327
  for(const v of vbls) {
3329
3328
  // NOTE: Prevent adding wildcard dataset modifiers more than once.
@@ -6242,19 +6241,19 @@ class Cluster extends NodeBox {
6242
6241
  }
6243
6242
 
6244
6243
  get nestingLevel() {
6245
- // Return the "depth" of this cluster in the cluster hierarchy
6244
+ // Return the "depth" of this cluster in the cluster hierarchy.
6246
6245
  if(this.cluster) return this.cluster.nestingLevel + 1; // recursion!
6247
6246
  return 0;
6248
6247
  }
6249
6248
 
6250
6249
  get toBeIgnored() {
6251
- // Return TRUE if this cluster or some parent cluster is set to be ignored
6250
+ // Return TRUE if this cluster or some parent cluster is set to be ignored.
6252
6251
  return this.ignore || MODEL.ignoreClusterInThisRun(this) ||
6253
6252
  (this.cluster && this.cluster.toBeIgnored); // recursion!
6254
6253
  }
6255
6254
 
6256
6255
  get blackBoxed() {
6257
- // Return TRUE if this cluster or some parent cluster is marked as black box
6256
+ // Return TRUE if this cluster or some parent cluster is marked as black box.
6258
6257
  return this.black_box ||
6259
6258
  (this.cluster && this.cluster.blackBoxed); // recursion!
6260
6259
  }
@@ -7930,6 +7929,7 @@ class Process extends Node {
7930
7929
  const a = {name: this.displayName};
7931
7930
  a.LB = this.lower_bound.asAttribute;
7932
7931
  a.UB = (this.equal_bounds ? a.LB : this.upper_bound.asAttribute);
7932
+ if(this.grid) a.LB = -a.UB;
7933
7933
  a.IL = this.initial_level.asAttribute;
7934
7934
  a.LCF = this.pace_expression.asAttribute;
7935
7935
  if(MODEL.solved) {
@@ -8863,13 +8863,13 @@ class Link {
8863
8863
  }
8864
8864
 
8865
8865
  get identifier() {
8866
- // NOTE: link IDs are based on the node codes rather than IDs, as this
8867
- // prevents problems when nodes are renamed
8866
+ // NOTE: Link IDs are based on the node codes rather than IDs, as this
8867
+ // prevents problems when nodes are renamed.
8868
8868
  return this.from_node.code + '___' + this.to_node.code;
8869
8869
  }
8870
8870
 
8871
8871
  get attributes() {
8872
- // NOTE: link is named by its tab-separated node names
8872
+ // NOTE: Link is named by its tab-separated node names.
8873
8873
  const a = {name: this.from_node.displayName + '\t' + this.to_node.displayName};
8874
8874
  a.R = this.relative_rate.asAttribute;
8875
8875
  if(MODEL.infer_cost_prices) a.SOC = this.share_of_cost;
@@ -9222,7 +9222,7 @@ class Dataset {
9222
9222
  }
9223
9223
 
9224
9224
  get attributes() {
9225
- // NOTE: modifiers are appended as additional lines of text
9225
+ // NOTE: Modifiers are appended as additional lines of text.
9226
9226
  const a = {name: this.displayName};
9227
9227
  a.D = '\t' + (this.vector ? this.vector[MODEL.t] : this.default_value);
9228
9228
  for(let k in this.modifiers) if(this.modifiers.hasOwnProperty(k)) {
@@ -9355,22 +9355,9 @@ class Dataset {
9355
9355
  }
9356
9356
 
9357
9357
  get dataString() {
9358
- // Data is stored simply as semicolon-separated floating point numbers,
9359
- // with N-digit precision to keep model files compact (default: N = 8).
9360
- let d = [];
9361
- // NOTE: Guard against empty strings and other invalid data.
9362
- for(const v of this.data) if(v) {
9363
- try {
9364
- // Convert number to string with the desired precision.
9365
- const f = v.toPrecision(CONFIGURATION.dataset_precision);
9366
- // Then parse it again, so that the number will be represented
9367
- // (by JavaScript) in the most compact representation.
9368
- d.push(parseFloat(f));
9369
- } catch(err) {
9370
- console.log('-- Notice: dataset', this.displayName, 'has invalid data', v);
9371
- }
9372
- }
9373
- return d.join(';');
9358
+ // NOTE: As of version 2.1.7, data is stored as semicolon-separated
9359
+ // B62-encoded numbers.
9360
+ return packVector(this.data);
9374
9361
  }
9375
9362
 
9376
9363
  get propertiesString() {
@@ -9389,10 +9376,17 @@ class Dataset {
9389
9376
  return ' (' + time_prop + (this.periodic ? ' \u21BB' : '') + ')';
9390
9377
  }
9391
9378
 
9392
- unpackDataString(str) {
9379
+ unpackDataString(str, b62=true) {
9393
9380
  // Convert semicolon-separated data to a numeric array.
9381
+ // NOTE: When b62 is FALSE, data is not B62-encoded.
9394
9382
  this.data.length = 0;
9395
- if(str) for(const n of str.split(';')) this.data.push(parseFloat(n));
9383
+ if(str) {
9384
+ if(b62) {
9385
+ this.data = unpackVector(str);
9386
+ } else {
9387
+ for(const n of str.split(';')) this.data.push(parseFloat(n));
9388
+ }
9389
+ }
9396
9390
  this.computeVector();
9397
9391
  this.computeStatistics();
9398
9392
  }
@@ -9611,24 +9605,31 @@ class Dataset {
9611
9605
  n = MODEL.black_box_entities[id];
9612
9606
  cmnts = '';
9613
9607
  }
9614
- const xml = ['<dataset', p, '><name>', xmlEncoded(n),
9615
- '</name><notes>', cmnts,
9616
- '</notes><default>', this.default_value,
9617
- '</default><unit>', xmlEncoded(this.scale_unit),
9618
- '</unit><time-scale>', this.time_scale,
9619
- '</time-scale><time-unit>', this.time_unit,
9620
- '</time-unit><method>', this.method,
9621
- '</method><url>', xmlEncoded(this.url),
9622
- '</url><data>', xmlEncoded(this.dataString),
9623
- '</data><modifiers>', ml.join(''),
9624
- '</modifiers><default-selector>', xmlEncoded(this.default_selector),
9625
- '</default-selector></dataset>'].join('');
9626
- return xml;
9608
+ const
9609
+ data = xmlEncoded(this.dataString),
9610
+ xml = ['<dataset', p, '>',
9611
+ '<name>', xmlEncoded(n),
9612
+ '</name><unit>', xmlEncoded(this.scale_unit),
9613
+ '</unit><time-scale>', this.time_scale,
9614
+ '</time-scale><time-unit>', this.time_unit,
9615
+ '</time-unit><method>', this.method,
9616
+ '</method>'];
9617
+ // NOTE: Omit empty fields to reduce model file size.
9618
+ if(cmnts) xml.push('<notes>', cmnts, '</notes>');
9619
+ if(this.default_value) xml.push('<default>',
9620
+ this.default_value, '</default>');
9621
+ if(this.url) xml.push('<url>', xmlEncoded(this.url), '</url>');
9622
+ if(data) xml.push('<data b62="1">', data, '</data>');
9623
+ if(ml.length) xml.push('<modifiers>', ml.join(''), '</modifiers>');
9624
+ if(this.default_selector) xml.push('<default-selector>',
9625
+ xmlEncoded(this.default_selector), '</default-selector>');
9626
+ xml.push('</dataset>');
9627
+ return xml.join('');
9627
9628
  }
9628
9629
 
9629
9630
  initFromXML(node) {
9630
9631
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
9631
- this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'));
9632
+ this.default_value = safeStrToFloat(nodeContentByTag(node, 'default'), 0);
9632
9633
  this.scale_unit = xmlDecoded(nodeContentByTag(node, 'unit')) || '1';
9633
9634
  this.time_scale = safeStrToFloat(nodeContentByTag(node, 'time-scale'), 1);
9634
9635
  this.time_unit = nodeContentByTag(node, 'time-unit') ||
@@ -9643,7 +9644,14 @@ class Dataset {
9643
9644
  if(this.url) {
9644
9645
  FILE_MANAGER.getRemoteData(this, this.url);
9645
9646
  } else {
9646
- this.unpackDataString(xmlDecoded(nodeContentByTag(node, 'data')));
9647
+ let data = '',
9648
+ b62 = false;
9649
+ const cn = childNodeByTag(node, 'data');
9650
+ if(cn) {
9651
+ data = nodeContent(cn);
9652
+ b62 = (nodeParameterValue(cn, 'b62') === '1');
9653
+ }
9654
+ this.unpackDataString(xmlDecoded(data), b62);
9647
9655
  }
9648
9656
  const n = childNodeByTag(node, 'modifiers');
9649
9657
  if(n) {
@@ -9981,8 +9989,8 @@ class ChartVariable {
9981
9989
  }
9982
9990
  // Scale the value unless run result (these are already scaled!).
9983
9991
  if(!rr) {
9984
- v *= this.scale_factor;
9985
9992
  if(this.absolute) v = Math.abs(v);
9993
+ v *= this.scale_factor;
9986
9994
  }
9987
9995
  this.vector.push(v);
9988
9996
  // Do not include values for t = 0 in statistics.
@@ -11372,83 +11380,30 @@ class ExperimentRunResult {
11372
11380
  return (this.attribute ? dn + '|' + this.attribute : dn);
11373
11381
  }
11374
11382
 
11375
- get vectorString() {
11376
- // Vector is coded as semicolon-separated floating point numbers
11377
- // reduced to N-digit precision to keep model files more compact.
11378
- // By default, N = 6; this can be altered in linny-r-config.js.
11379
- // To represent "sparse" vectors more compactly, sequences of
11380
- // identical values are encoded as NxF where N is the length of
11381
- // the sequence and F the numerical value, e.g., "17x0.4;0.2;7x0"
11382
- if(this.was_ignored) return '';
11383
- let prev = '',
11384
- cnt = 1;
11385
- const vl = [];
11386
- for(const v of this.vector) {
11387
- // Format number with desired precision.
11388
- const f = v.toPrecision(CONFIGURATION.results_precision);
11389
- // While value is same as previous, do not store, but count.
11390
- if(f === prev) {
11391
- cnt++;
11392
- } else {
11393
- if(cnt > 1) {
11394
- // More than one => "compress".
11395
- // NOTE: Parse so JavaScript will represent it most compactly.
11396
- vl.push(cnt + 'x' + parseFloat(prev));
11397
- cnt = 1;
11398
- } else if(prev) {
11399
- vl.push(parseFloat(prev));
11400
- }
11401
- prev = f;
11402
- }
11403
- }
11404
- // Add the last "batch" of numbers.
11405
- if(cnt > 1) {
11406
- // More than one => "compress".
11407
- // NOTE: Parse so JavaScript will represent it most compactly.
11408
- vl.push(cnt + 'x' + parseFloat(prev));
11409
- cnt = 1;
11410
- } else if(prev) {
11411
- vl.push(parseFloat(prev));
11412
- }
11413
- return vl.join(';');
11414
- }
11415
-
11416
- unpackVectorString(str) {
11417
- // Convert semicolon-separated data to a numeric array.
11418
- this.vector = [];
11419
- if(str && !this.was_ignored) {
11420
- for(const parts of str.split(';')) {
11421
- const tuple = parts.split('x');
11422
- if(tuple.length === 2) {
11423
- const
11424
- n = parseInt(tuple[0]),
11425
- f = parseFloat(tuple[1]);
11426
- for(let i = 0; i < n; i++) {
11427
- this.vector.push(f);
11428
- }
11429
- } else {
11430
- this.vector.push(parseFloat(tuple[0]));
11431
- }
11432
- }
11433
- }
11434
- }
11435
-
11436
11383
  get asXML() {
11437
- return ['<run-result', (this.x_variable ? ' x-variable="1"' : ''),
11438
- (this.was_ignored ? ' ignored="1"' : ''),
11439
- '><object-id>', xmlEncoded(this.object_id),
11440
- '</object-id><attribute>', xmlEncoded(this.attribute),
11441
- '</attribute><count>', this.N,
11442
- '</count><sum>', this.sum,
11443
- '</sum><mean>', this.mean,
11444
- '</mean><variance>', this.variance,
11445
- '</variance><minimum>', this.minimum,
11446
- '</minimum><maximum>', this.maximum,
11447
- '</maximum><non-zero-tally>', this.non_zero_tally,
11448
- '</non-zero-tally><last>', this.last,
11449
- '</last><exceptions>', this.exceptions,
11450
- '</exceptions><vector>', this.vectorString,
11451
- '</vector></run-result>'].join('');
11384
+ const
11385
+ data = (this.was_ignored ? '' : packVector(this.vector)),
11386
+ xml = ['<run-result',
11387
+ (this.x_variable ? ' x-variable="1"' : ''),
11388
+ (this.was_ignored ? ' ignored="1"' : ''),
11389
+ '><object-id>', xmlEncoded(this.object_id),
11390
+ '</object-id><attribute>', xmlEncoded(this.attribute),
11391
+ '</attribute>'];
11392
+ // NOTE: Reduce model size by saving only non-zero statistics.
11393
+ if(this.N) xml.push('<count>', this.N, '</count>');
11394
+ if(this.sum) xml.push('<sum>', this.sum, '</sum>');
11395
+ if(this.mean) xml.push('<mean>', this.mean, '</mean>');
11396
+ if(this.variance) xml.push('<variance>', this.variance, '</variance>');
11397
+ if(this.minimum) xml.push('<minimum>', this.minimum, '</minimum>');
11398
+ if(this.maximum) xml.push('<maximum>', this.maximum, '</maximum>');
11399
+ if(this.non_zero_tally) xml.push('<non-zero-tally>',
11400
+ this.non_zero_tally, '</non-zero-tally>');
11401
+ if(this.last) xml.push('<last>', this.last, '</last>');
11402
+ if(this.exceptions) xml.push('<exceptions>',
11403
+ this.exceptions, '</exceptions>');
11404
+ if(data) xml.push('<vector b62="1">', data, '</vector>');
11405
+ xml.push('</run-result>');
11406
+ return xml.join('');
11452
11407
  }
11453
11408
 
11454
11409
  initFromXML(node) {
@@ -11461,16 +11416,22 @@ class ExperimentRunResult {
11461
11416
  if(this.object_id === UI.EQUATIONS_DATASET_ID &&
11462
11417
  !earlierVersion(MODEL.version, '1.3.0')) attr = xmlDecoded(attr);
11463
11418
  this.attribute = attr;
11464
- this.N = safeStrToInt(nodeContentByTag(node, 'count'));
11465
- this.sum = safeStrToFloat(nodeContentByTag(node, 'sum'));
11466
- this.mean = safeStrToFloat(nodeContentByTag(node, 'mean'));
11467
- this.variance = safeStrToFloat(nodeContentByTag(node, 'variance'));
11468
- this.minimum = safeStrToFloat(nodeContentByTag(node, 'minimum'));
11469
- this.maximum = safeStrToFloat(nodeContentByTag(node, 'maximum'));
11470
- this.non_zero_tally = safeStrToInt(nodeContentByTag(node, 'non-zero-tally'));
11471
- this.last = safeStrToInt(nodeContentByTag(node, 'last'));
11472
- this.exceptions = safeStrToInt(nodeContentByTag(node, 'exceptions'));
11473
- this.unpackVectorString(nodeContentByTag(node, 'vector'));
11419
+ this.N = safeStrToInt(nodeContentByTag(node, 'count'), 0);
11420
+ this.sum = safeStrToFloat(nodeContentByTag(node, 'sum'), 0);
11421
+ this.mean = safeStrToFloat(nodeContentByTag(node, 'mean'), 0);
11422
+ this.variance = safeStrToFloat(nodeContentByTag(node, 'variance'), 0);
11423
+ this.minimum = safeStrToFloat(nodeContentByTag(node, 'minimum'), 0);
11424
+ this.maximum = safeStrToFloat(nodeContentByTag(node, 'maximum'), 0);
11425
+ this.non_zero_tally = safeStrToInt(nodeContentByTag(node, 'non-zero-tally'), 0);
11426
+ this.last = safeStrToFloat(nodeContentByTag(node, 'last'), 0);
11427
+ this.exceptions = safeStrToInt(nodeContentByTag(node, 'exceptions'), 0);
11428
+ const cn = childNodeByTag(node, 'vector');
11429
+ if(cn && !this.was_ignored) {
11430
+ const b62 = nodeParameterValue(cn, 'b62') === '1';
11431
+ this.vector = unpackVector(nodeContent(cn), b62);
11432
+ } else {
11433
+ this.vector = [];
11434
+ }
11474
11435
  }
11475
11436
 
11476
11437
  valueAtModelTime(t, mtsd, method, periodic) {
@@ -11597,7 +11558,7 @@ class ExperimentRun {
11597
11558
  UI.warn(`Run title "${t}" does not match experiment title "` +
11598
11559
  this.experiment.title + '"');
11599
11560
  }
11600
- this.combi = nodeContentByTag(node, 'x-combi').split(' ');
11561
+ this.combination = nodeContentByTag(node, 'x-combi').split(' ');
11601
11562
  this.time_steps = safeStrToInt(nodeContentByTag(node, 'time-steps'));
11602
11563
  this.time_step_duration = safeStrToFloat(nodeContentByTag(node, 'delta-t'));
11603
11564
  let n = childNodeByTag(node, 'results');
@@ -13051,7 +13012,7 @@ class Constraint {
13051
13012
  }
13052
13013
 
13053
13014
  get typeLetter() {
13054
- return 'C';
13015
+ return 'B';
13055
13016
  }
13056
13017
 
13057
13018
  get identifier() {
@@ -53,6 +53,174 @@ function postData(obj) {
53
53
  // Functions that convert numbers to strings, or strings to numbers
54
54
  //
55
55
 
56
+ const b62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
57
+
58
+ function posIntToB62(n) {
59
+ // Return the radix-62 encoding of positive integer value `n`.
60
+ let s = '';
61
+ while(n > 0) {
62
+ s = b62.charAt(n % 62) + s;
63
+ n = Math.floor(n / 62);
64
+ }
65
+ return s;
66
+ }
67
+
68
+ function B62ToInt(s) {
69
+ // Return the value of a B62-encoded integer, or -1 if `s` contains an
70
+ // invalid character.
71
+ const digits = s.split('');
72
+ let n = 0;
73
+ for(const d of digits) {
74
+ const index = b62.indexOf(d);
75
+ if(index < 0) return -1;
76
+ n = n * 62 + index;
77
+ }
78
+ return n;
79
+ }
80
+
81
+ function packFloat(f) {
82
+ // Return floating point number `f` in a compact string notation.
83
+ // The first character encodes whether `f` is an integer (then no exponent),
84
+ // whether the sign of `f` is + or -, and whether the sign of the exponent
85
+ // (if any) is + or - in the following manner:
86
+ // (no leading special character) => positive integer (no exponent)
87
+ // - negative integer (no exponent)
88
+ // ^ positive number, positive exponent
89
+ // _ negative number, positive exponent
90
+ // ~ positive number, negative exponent
91
+ // = negative number, negative exponent
92
+ const oldf = f;
93
+ if(!f) return '0';
94
+ let sign = '',
95
+ mant = '',
96
+ exp = 0;
97
+ if(f < 0.0) {
98
+ sign = '-';
99
+ f = -f;
100
+ }
101
+ const rf = Math.round(f);
102
+ // When number is integer, it has no exponent part.
103
+ if(rf === f) return sign + posIntToB62(rf);
104
+ const me = f.toExponential().split('e');
105
+ // Remove the decimal period from the mantissa.
106
+ mant = posIntToB62(parseInt(me[0].replace('.', '')));
107
+ // Determine the exponent part and its sign.
108
+ exp = parseInt(me[1]);
109
+ if(exp < 0) {
110
+ exp = -exp;
111
+ sign = (sign ? '=' : '~');
112
+ } else {
113
+ sign = (sign ? '_' : '^');
114
+ }
115
+ // NOTE: Exponent is always codes as a single character. This limits
116
+ // its value to 61, which for Linny-R suffices to code even its error
117
+ // values (highest error code exponent is +50).
118
+ return sign + mant + (exp ? posIntToB62(Math.min(exp, 61)) : '0');
119
+ }
120
+
121
+ function unpackFloat(s) {
122
+ // Return the number X that was encoded using packFloat(X).
123
+ // NOTE: When decoding fails, -1e+48 is returned to signal #INVALID.
124
+ if(s === '0') return 0;
125
+ const
126
+ INVALID = -1e+48,
127
+ ss = s.split('');
128
+ const index = '-^_~='.indexOf(ss[0]);
129
+ if(index < 1) {
130
+ // No exponent => get the absolute integer value.
131
+ const n = B62ToInt(s.substring(index + 1));
132
+ if(n < 0) return INVALID;
133
+ // Return the signed integer value.
134
+ return (index ? n : -n);
135
+ }
136
+ // Now the last character codes the exponent.
137
+ const
138
+ // Odd index (1 and 3) indicates positive number.
139
+ sign = (index % 2 ? '' : '-'),
140
+ // Low index (1 and 2) indicates positive exponent.
141
+ esign = (index < 3 ? 'e+' : 'e-');
142
+ // Remove the sign character.
143
+ ss.shift();
144
+ // Get and remove the exponent character, and decode it.
145
+ let exp = B62ToInt(ss.pop());
146
+ if(exp < 0) return INVALID;
147
+ exp = esign + exp.toString();
148
+ let mant = B62ToInt(ss.join(''));
149
+ if(mant < 0) return INVALID;
150
+ mant = mant.toString();
151
+ // NOTE: No decimal point if mantissa is a single decimal digit.
152
+ if(mant.length > 1) mant = mant.slice(0, 1) + '.' + mant.slice(1);
153
+ return parseFloat(sign + mant + exp);
154
+ }
155
+
156
+ function packVector(vector) {
157
+ // Vector is coded as semicolon-separated B62-encoded floating
158
+ // point numbers (no precision loss).
159
+ // To represent "sparse" vectors more compactly, sequences of
160
+ // identical values are encoded as N*F where N is the length of
161
+ // the sequence and F the B62-encoded numerical value.
162
+ let prev = false,
163
+ cnt = 0;
164
+ const vl = [];
165
+ for(const v of vector) {
166
+ // While value is same as previous, do not store, but count.
167
+ // NOTE: JavaScript precision is about 15 decimals, so test for
168
+ // equality with this precision.
169
+ if(prev === false || Math.abs(v - prev) < 0.5e-14) {
170
+ cnt++;
171
+ } else {
172
+ const b62 = packFloat(prev);
173
+ if(cnt > 1) {
174
+ // More than one => "compress".
175
+ vl.push(cnt + '*' + b62);
176
+ cnt = 1;
177
+ } else {
178
+ vl.push(b62);
179
+ }
180
+ }
181
+ prev = v;
182
+ }
183
+ if(cnt) {
184
+ const b62 = packFloat(prev);
185
+ // Add the last "batch" of numbers.
186
+ if(cnt > 1) {
187
+ // More than one => "compress".
188
+ vl.push(cnt + '*' + b62);
189
+ } else {
190
+ vl.push(b62);
191
+ }
192
+ }
193
+ return vl.join(';');
194
+ }
195
+
196
+ function unpackVector(str, b62=true) {
197
+ // Convert semicolon-separated data to a numeric array.
198
+ // NOTE: Until version 2.1.7, numbers were represented in standard
199
+ // decimal notation with limited precision. From v2.1.7 onwards, numbers
200
+ // are B62-encoded. When `b62` is FALSE, the legacy decoding is used.
201
+ vector = [];
202
+ if(str) {
203
+ const
204
+ ss = str.split(';'),
205
+ multi = (b62 ? '*' : 'x'),
206
+ parse = (b62 ? unpackFloat : parseFloat);
207
+ for(const parts of ss) {
208
+ const tuple = parts.split(multi);
209
+ if(tuple.length === 2) {
210
+ const f = parse(tuple[1]);
211
+ let n = parseInt(tuple[0]);
212
+ while(n > 0) {
213
+ vector.push(f);
214
+ n--;
215
+ }
216
+ } else {
217
+ vector.push(parse(tuple[0]));
218
+ }
219
+ }
220
+ }
221
+ return vector;
222
+ }
223
+
56
224
  function pluralS(n, s, special='') {
57
225
  // Returns string with noun `s` in singular only if `n` = 1
58
226
  // NOTE: third parameter can be used for nouns with irregular plural form
@@ -189,11 +357,16 @@ function uniformDecimals(data) {
189
357
  }
190
358
  maxi = Math.max(maxi, ss[0].length);
191
359
  }
192
- // STEP 2: Convert the data to a uniform format
360
+ // STEP 2: Convert the data to a uniform format.
361
+ const special = ['\u221E', '-\u221E', '\u2047', '\u00A2'];
193
362
  for(let i = 0; i < data.length; i++) {
194
- const f = parseFloat(data[i]);
363
+ const
364
+ v = data[i],
365
+ f = parseFloat(v);
195
366
  if(isNaN(f)) {
196
- data[i] = '\u26A0'; // Unicode warning sign
367
+ // Keep special values such as infinity, and replace error values
368
+ // by Unicode warning sign.
369
+ if(special.indexOf(v) < 0) data[i] = '\u26A0';
197
370
  } else if(maxe > 0) {
198
371
  // Convert ALL numbers to exponential notation with two decimals (1.23e+7)
199
372
  const v = f.toExponential(2);
@@ -405,51 +578,59 @@ function patternList(str) {
405
578
 
406
579
  function patternMatch(str, patterns) {
407
580
  // Returns TRUE when `str` matches the &|^-pattern.
408
- // NOTE: If a pattern starts with equals sign = then `str` must
409
- // equal the rest of the pattern to match; if it starts with a tilde
410
- // ~ then `str` must start with the rest of the pattern to match.
581
+ // NOTE: If a pattern starts with a tilde ~ then `str` must start with
582
+ // the rest of the pattern to match. If it ends with a tilde, then `str`
583
+ // must end with the first part of the pattern.
584
+ // In this way, ~pattern~ denotes that `str` should exactly match.
411
585
  for(let i = 0; i < patterns.length; i++) {
412
586
  const p = patterns[i];
413
587
  // NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
414
588
  // sub-sub-patterns (all of which should match) and a set of "min"
415
589
  // sub-sub-patters (all should NOT match)
416
590
  let pm,
591
+ swt,
592
+ ewt,
417
593
  re,
418
594
  match = true;
419
595
  for(let j = 0; match && j < p.plus.length; j++) {
420
596
  pm = p.plus[j];
421
- if(pm.startsWith('=')) {
422
- match = (str === pm.substring(1));
423
- } else if(pm.startsWith('~')) {
597
+ swt = pm.startsWith('~');
598
+ ewt = pm.endsWith('~');
599
+ if(swt && ewt) {
600
+ match = (str === pm.slice(1, -1));
601
+ } else if(swt) {
424
602
  match = str.startsWith(pm.substring(1));
603
+ } else if(ewt) {
604
+ match = str.endsWith(pm.slice(0, -1));
425
605
  } else {
426
606
  match = (str.indexOf(pm) >= 0);
427
607
  }
428
- // If no match, check whether pattern contains wildcards
608
+ // If no match, check whether pattern contains wildcards.
429
609
  if(!match && pm.indexOf('#') >= 0) {
430
610
  // If so, rematch using regular expression that tests for a
431
- // number or a ?? wildcard
611
+ // number or a ?? wildcard.
432
612
  let res = pm.split('#');
433
613
  for(let i = 0; i < res.length; i++) {
434
614
  res[i] = escapeRegex(res[i]);
435
615
  }
436
616
  res = res.join('(\\d+|\\?\\?)');
437
- if(pm.startsWith('=')) {
438
- res = '^' + res + '$';
439
- } else if(pm.startsWith('~')) {
440
- res = '^' + res;
441
- }
617
+ if(swt) res = '^' + res;
618
+ if(ewt) res += '$';
442
619
  re = new RegExp(res, 'g');
443
620
  match = re.test(str);
444
621
  }
445
622
  }
446
- // Any "min" match indicates NO match for this sub-pattern,
623
+ // Any "min" match indicates NO match for this sub-pattern.
447
624
  for(let j = 0; match && j < p.min.length; j++) {
448
625
  pm = p.min[j];
449
- if(pm.startsWith('=')) {
450
- match = (str !== pm.substring(1));
451
- } else if(pm.startsWith('~')) {
626
+ swt = pm.startsWith('~');
627
+ ewt = pm.endsWith('~');
628
+ if(swt && ewt) {
629
+ match = (str !== pm.slice(1, -1));
630
+ } else if(swt) {
452
631
  match = !str.startsWith(pm.substring(1));
632
+ } else if(ewt) {
633
+ match = !str.endsWith(pm.slice(0, -1));
453
634
  } else {
454
635
  match = (str.indexOf(pm) < 0);
455
636
  }
@@ -461,11 +642,8 @@ function patternMatch(str, patterns) {
461
642
  res[i] = escapeRegex(res[i]);
462
643
  }
463
644
  res = res.join('(\\d+|\\?\\?)');
464
- if(pm.startsWith('=')) {
465
- res = '^' + res + '$';
466
- } else if(pm.startsWith('~')) {
467
- res = '^' + res;
468
- }
645
+ if(swt) res = '^' + res;
646
+ if(ewt) res += '$';
469
647
  re = new RegExp(res, 'g');
470
648
  match = !re.test(str);
471
649
  }
@@ -973,18 +1151,20 @@ function nameToLines(name, actor_name = '') {
973
1151
  // the node box.
974
1152
  let m = actor_name.length;
975
1153
  const
976
- d = Math.floor(Math.sqrt(0.3 * name.length)),
1154
+ d = Math.floor(Math.sqrt(0.25 * name.length)),
977
1155
  // Do not wrap strings shorter than 13 characters (about 50 pixels).
978
1156
  limit = Math.max(Math.ceil(name.length / d), m, 13),
979
- a = name.split(' ');
980
- // Split words at '-' when wider than limit
1157
+ // NOTE: Do not split on spaces followed by a number or a single
1158
+ // capital letter.
1159
+ a = name.split(/\s(?!\d+:|\d+$|[A-Z]\W)/);
1160
+ // Split words at '-' when wider than limit.
981
1161
  for(let j = 0; j < a.length; j++) {
982
1162
  if(a[j].length > limit) {
983
1163
  const sw = a[j].split('-');
984
1164
  if(sw.length > 1) {
985
- // Replace j-th word by last fragment of split string
1165
+ // Replace j-th word by last fragment of split string.
986
1166
  a[j] = sw.pop();
987
- // Insert remaining fragments before
1167
+ // Insert remaining fragments before.
988
1168
  while(sw.length > 0) a.splice(j, 0, sw.pop() + '-');
989
1169
  }
990
1170
  }