linny-r 2.1.6 → 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.
package/package.json
CHANGED
@@ -11,7 +11,7 @@ warning or information messages are displayed.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -41,7 +41,7 @@ const CONFIGURATION = {
|
|
41
41
|
// To keep model files compact, floating point values in datasets and run
|
42
42
|
// results are stored with a limited number of significant digits
|
43
43
|
dataset_precision: 8,
|
44
|
-
results_precision:
|
44
|
+
results_precision: 8,
|
45
45
|
// Default properties for new models
|
46
46
|
default_currency_unit: 'EUR',
|
47
47
|
default_time_unit: 'hour',
|
@@ -90,7 +90,8 @@ class GUIFileManager {
|
|
90
90
|
if(!UI.hidden('series-modal')) {
|
91
91
|
DATASET_MANAGER.series_data.value = data.split(';').join('\n');
|
92
92
|
} else {
|
93
|
-
|
93
|
+
// NOTE: FALSE indicates that data is *not* B62-encoded.
|
94
|
+
dataset.unpackDataString(data, false);
|
94
95
|
}
|
95
96
|
}
|
96
97
|
// NOTE: remove dataset from the "loading" list
|
@@ -9355,22 +9355,9 @@ class Dataset {
|
|
9355
9355
|
}
|
9356
9356
|
|
9357
9357
|
get dataString() {
|
9358
|
-
//
|
9359
|
-
//
|
9360
|
-
|
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)
|
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
|
9615
|
-
|
9616
|
-
'
|
9617
|
-
|
9618
|
-
|
9619
|
-
|
9620
|
-
|
9621
|
-
|
9622
|
-
|
9623
|
-
|
9624
|
-
|
9625
|
-
|
9626
|
-
|
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
|
-
|
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) {
|
@@ -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
|
-
|
11438
|
-
|
11439
|
-
|
11440
|
-
|
11441
|
-
|
11442
|
-
|
11443
|
-
|
11444
|
-
|
11445
|
-
|
11446
|
-
|
11447
|
-
|
11448
|
-
|
11449
|
-
|
11450
|
-
|
11451
|
-
|
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 =
|
11472
|
-
this.exceptions = safeStrToInt(nodeContentByTag(node, 'exceptions'));
|
11473
|
-
|
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.
|
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');
|
@@ -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
|
@@ -410,29 +578,29 @@ function patternList(str) {
|
|
410
578
|
|
411
579
|
function patternMatch(str, patterns) {
|
412
580
|
// Returns TRUE when `str` matches the &|^-pattern.
|
413
|
-
// NOTE: If a pattern starts with
|
414
|
-
//
|
415
|
-
//
|
416
|
-
// In this way,
|
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.
|
417
585
|
for(let i = 0; i < patterns.length; i++) {
|
418
586
|
const p = patterns[i];
|
419
587
|
// NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
|
420
588
|
// sub-sub-patterns (all of which should match) and a set of "min"
|
421
589
|
// sub-sub-patters (all should NOT match)
|
422
590
|
let pm,
|
423
|
-
|
424
|
-
|
591
|
+
swt,
|
592
|
+
ewt,
|
425
593
|
re,
|
426
594
|
match = true;
|
427
595
|
for(let j = 0; match && j < p.plus.length; j++) {
|
428
596
|
pm = p.plus[j];
|
429
|
-
|
430
|
-
|
431
|
-
if(
|
597
|
+
swt = pm.startsWith('~');
|
598
|
+
ewt = pm.endsWith('~');
|
599
|
+
if(swt && ewt) {
|
432
600
|
match = (str === pm.slice(1, -1));
|
433
|
-
} else if(
|
601
|
+
} else if(swt) {
|
434
602
|
match = str.startsWith(pm.substring(1));
|
435
|
-
} else if(
|
603
|
+
} else if(ewt) {
|
436
604
|
match = str.endsWith(pm.slice(0, -1));
|
437
605
|
} else {
|
438
606
|
match = (str.indexOf(pm) >= 0);
|
@@ -446,8 +614,8 @@ function patternMatch(str, patterns) {
|
|
446
614
|
res[i] = escapeRegex(res[i]);
|
447
615
|
}
|
448
616
|
res = res.join('(\\d+|\\?\\?)');
|
449
|
-
if(
|
450
|
-
if(
|
617
|
+
if(swt) res = '^' + res;
|
618
|
+
if(ewt) res += '$';
|
451
619
|
re = new RegExp(res, 'g');
|
452
620
|
match = re.test(str);
|
453
621
|
}
|
@@ -455,13 +623,13 @@ function patternMatch(str, patterns) {
|
|
455
623
|
// Any "min" match indicates NO match for this sub-pattern.
|
456
624
|
for(let j = 0; match && j < p.min.length; j++) {
|
457
625
|
pm = p.min[j];
|
458
|
-
|
459
|
-
|
460
|
-
if(
|
626
|
+
swt = pm.startsWith('~');
|
627
|
+
ewt = pm.endsWith('~');
|
628
|
+
if(swt && ewt) {
|
461
629
|
match = (str !== pm.slice(1, -1));
|
462
|
-
} else if(
|
630
|
+
} else if(swt) {
|
463
631
|
match = !str.startsWith(pm.substring(1));
|
464
|
-
} else if(
|
632
|
+
} else if(ewt) {
|
465
633
|
match = !str.endsWith(pm.slice(0, -1));
|
466
634
|
} else {
|
467
635
|
match = (str.indexOf(pm) < 0);
|
@@ -474,8 +642,8 @@ function patternMatch(str, patterns) {
|
|
474
642
|
res[i] = escapeRegex(res[i]);
|
475
643
|
}
|
476
644
|
res = res.join('(\\d+|\\?\\?)');
|
477
|
-
if(
|
478
|
-
if(
|
645
|
+
if(swt) res = '^' + res;
|
646
|
+
if(ewt) res += '$';
|
479
647
|
re = new RegExp(res, 'g');
|
480
648
|
match = !re.test(str);
|
481
649
|
}
|
@@ -7249,14 +7249,14 @@ function VMI_push_run_result(x, args) {
|
|
7249
7249
|
if(Array.isArray(rn)) {
|
7250
7250
|
// Let the running experiment infer run number from selector list `rn`
|
7251
7251
|
// and its own "active combination" of selectors.
|
7252
|
-
rn = xp.matchingCombinationIndex(rn);
|
7252
|
+
rn = xp.matchingCombinationIndex(rn);
|
7253
7253
|
} else if(rn < 0) {
|
7254
7254
|
// Relative run number: use current run # + r (first run has number 0).
|
7255
7255
|
if(xp === MODEL.running_experiment) {
|
7256
7256
|
rn += xp.active_combination_index;
|
7257
7257
|
} else if(xp.chart_combinations.length) {
|
7258
7258
|
// Modeler has selected one or more runs in the viewer table.
|
7259
|
-
//
|
7259
|
+
// Find the highest number of a selected run that has been performed.
|
7260
7260
|
let last = -1;
|
7261
7261
|
for(const ccn of xp.chart_combinations) {
|
7262
7262
|
if(ccn > last && ccn < xp.runs.length) last = ccn;
|