linny-r 1.2.0 → 1.3.0
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/README.md +6 -6
- package/console.js +2 -2
- package/package.json +2 -2
- package/static/images/paste.png +0 -0
- package/static/index.html +55 -6
- package/static/linny-r.css +100 -22
- package/static/scripts/linny-r-ctrl.js +26 -4
- package/static/scripts/linny-r-gui.js +940 -119
- package/static/scripts/linny-r-model.js +203 -37
- package/static/scripts/linny-r-utils.js +49 -11
- package/static/scripts/linny-r-vm.js +29 -18
@@ -120,6 +120,9 @@ class LinnyRModel {
|
|
120
120
|
// Set the indicator that the model has not been solved yet
|
121
121
|
this.set_up = false;
|
122
122
|
this.solved = false;
|
123
|
+
// Reset counts of effects of a rename operation
|
124
|
+
this.variable_count = 0;
|
125
|
+
this.expression_count = 0;
|
123
126
|
}
|
124
127
|
|
125
128
|
// NOTE: a model can also be the entity for the documentation manager,
|
@@ -557,7 +560,7 @@ class LinnyRModel {
|
|
557
560
|
inferPrefix(obj) {
|
558
561
|
// Return the inferred (!) prefixes of `obj` as a list
|
559
562
|
if(obj) {
|
560
|
-
const pl = obj.displayName
|
563
|
+
const pl = UI.prefixesAndName(obj.displayName);
|
561
564
|
if(pl.length > 1) {
|
562
565
|
pl.pop();
|
563
566
|
return pl;
|
@@ -565,7 +568,7 @@ class LinnyRModel {
|
|
565
568
|
}
|
566
569
|
return [];
|
567
570
|
}
|
568
|
-
|
571
|
+
|
569
572
|
inferParentCluster(obj) {
|
570
573
|
// Find the best "parent" cluster for link or constraint `obj`
|
571
574
|
let p, q;
|
@@ -633,6 +636,14 @@ class LinnyRModel {
|
|
633
636
|
}
|
634
637
|
return -1;
|
635
638
|
}
|
639
|
+
|
640
|
+
isDimensionSelector(s) {
|
641
|
+
// Returns TRUE if `s` is a dimension selector in some experiment
|
642
|
+
for(let i = 0; i < this.experiments.length; i++) {
|
643
|
+
if(this.experiments[i].isDimensionSelector(s)) return true;
|
644
|
+
}
|
645
|
+
return false;
|
646
|
+
}
|
636
647
|
|
637
648
|
canLink(from, to) {
|
638
649
|
// Return TRUE iff FROM-node can feature a "straight" link (i.e., a
|
@@ -689,8 +700,8 @@ class LinnyRModel {
|
|
689
700
|
// Merge into dimension if there are shared selectors
|
690
701
|
for(let i = 0; i < this.dimensions.length; i++) {
|
691
702
|
const c = complement(sl, this.dimensions[i]);
|
692
|
-
if(c.length
|
693
|
-
this.dimensions[i].push(...c);
|
703
|
+
if(c.length < sl.length) {
|
704
|
+
if(c.length > 0) this.dimensions[i].push(...c);
|
694
705
|
newdim = false;
|
695
706
|
break;
|
696
707
|
}
|
@@ -915,7 +926,7 @@ class LinnyRModel {
|
|
915
926
|
for(let i = 0; i < c.notes.length; i++) {
|
916
927
|
const
|
917
928
|
n = c.notes[i],
|
918
|
-
tags = n.
|
929
|
+
tags = n.tagList;
|
919
930
|
if(tags) {
|
920
931
|
for(let i = 0; i < tags.length; i++) {
|
921
932
|
const
|
@@ -1181,8 +1192,8 @@ class LinnyRModel {
|
|
1181
1192
|
}
|
1182
1193
|
const id = UI.nameToID(name);
|
1183
1194
|
let d = this.namedObjectByID(id);
|
1184
|
-
if(d) {
|
1185
|
-
if(IO_CONTEXT
|
1195
|
+
if(d && d !== this.equations_dataset) {
|
1196
|
+
if(IO_CONTEXT) {
|
1186
1197
|
IO_CONTEXT.supersede(d);
|
1187
1198
|
} else {
|
1188
1199
|
// Preserve name uniqueness
|
@@ -1205,8 +1216,6 @@ class LinnyRModel {
|
|
1205
1216
|
if(eqds) {
|
1206
1217
|
// Restore pointer to original equations dataset
|
1207
1218
|
this.equations_dataset = eqds;
|
1208
|
-
// Add included equations with prefixed names
|
1209
|
-
console.log('HERE', d);
|
1210
1219
|
// Return the extended equations dataset
|
1211
1220
|
return eqds;
|
1212
1221
|
} else {
|
@@ -1564,6 +1573,75 @@ console.log('HERE', d);
|
|
1564
1573
|
return true;
|
1565
1574
|
}
|
1566
1575
|
|
1576
|
+
get selectionAsXML() {
|
1577
|
+
// Returns XML for the selected entities, and also for the entities
|
1578
|
+
// referenced by expressions for their attributes.
|
1579
|
+
// NOTE: the name and actor name of the focal cluster are added as
|
1580
|
+
// attributes of the main node to permit "smart" renaming of
|
1581
|
+
// entities when PASTE would result in name conflicts.
|
1582
|
+
if(this.selection.length <= 0) return '';
|
1583
|
+
const
|
1584
|
+
fc_name = this.focal_cluster.name,
|
1585
|
+
fc_actor = this.focal_cluster.actor.name,
|
1586
|
+
entities = {
|
1587
|
+
Cluster: [],
|
1588
|
+
Link: [],
|
1589
|
+
Constraint: [],
|
1590
|
+
Note: [],
|
1591
|
+
Product: [],
|
1592
|
+
Process: []
|
1593
|
+
},
|
1594
|
+
extras = [],
|
1595
|
+
from_tos = [],
|
1596
|
+
xml = [],
|
1597
|
+
ft_xml = [],
|
1598
|
+
extra_xml = [];
|
1599
|
+
for(let i = 0; i < this.selection.length; i++) {
|
1600
|
+
const obj = this.selection[i];
|
1601
|
+
entities[obj.type].push(obj);
|
1602
|
+
}
|
1603
|
+
for(let i = 0; i < entities.Note.length; i++) {
|
1604
|
+
const n = entities.Note[i];
|
1605
|
+
xml.push(n.asXML);
|
1606
|
+
}
|
1607
|
+
for(let i = 0; i < entities.Product.length; i++) {
|
1608
|
+
xml.push(entities.Product[i].asXML);
|
1609
|
+
}
|
1610
|
+
for(let i = 0; i < entities.Process.length; i++) {
|
1611
|
+
xml.push(entities.Process[i].asXML);
|
1612
|
+
}
|
1613
|
+
for(let i = 0; i < entities.Cluster.length; i++) {
|
1614
|
+
xml.push(entities.Cluster[i].asXML);
|
1615
|
+
}
|
1616
|
+
for(let i = 0; i < entities.Link.length; i++) {
|
1617
|
+
const l = entities.Link[i];
|
1618
|
+
if(this.selection.indexOf(l.from_node) < 0) addDistinct(l.from_node, from_tos);
|
1619
|
+
if(this.selection.indexOf(l.to_node) < 0) addDistinct(l.to_node, from_tos);
|
1620
|
+
xml.push(l.asXML);
|
1621
|
+
}
|
1622
|
+
for(let i = 0; i < entities.Constraint.length; i++) {
|
1623
|
+
const c = entities.Constraint[i];
|
1624
|
+
if(this.selection.indexOf(c.from_node) < 0) addDistinct(c.from_node, from_tos);
|
1625
|
+
if(this.selection.indexOf(c.to_node) < 0) addDistinct(c.to_node, from_tos);
|
1626
|
+
xml.push(c.asXML);
|
1627
|
+
}
|
1628
|
+
for(let i = 0; i < from_tos.length; i++) {
|
1629
|
+
const p = from_tos[i];
|
1630
|
+
ft_xml.push('<from-to type="', p.type, '" name="', xmlEncoded(p.name));
|
1631
|
+
if(p instanceof Process) ft_xml.push('" actor-name="', xmlEncoded(p.actor.name));
|
1632
|
+
ft_xml.push('"></from-to>');
|
1633
|
+
}
|
1634
|
+
for(let i = 0; i < extras.length; i++) extra_xml.push(extras[i].asXML);
|
1635
|
+
return ['<copy timestamp="', Date.now(),
|
1636
|
+
'" model-timestamp="', this.time_created.getTime(),
|
1637
|
+
'" cluster-name="', xmlEncoded(fc_name),
|
1638
|
+
'" cluster-actor="', xmlEncoded(fc_actor),
|
1639
|
+
'"><entities>', xml.join(''),
|
1640
|
+
'</entities><from-tos>', ft_xml.join(''),
|
1641
|
+
'</from-tos><extras>', extra_xml.join(''),
|
1642
|
+
'</extras></copy>'].join('');
|
1643
|
+
}
|
1644
|
+
|
1567
1645
|
dropSelectionIntoCluster(c) {
|
1568
1646
|
// Move all selected nodes to cluster `c`
|
1569
1647
|
let n = 0,
|
@@ -2026,46 +2104,74 @@ console.log('HERE', d);
|
|
2026
2104
|
}
|
2027
2105
|
}
|
2028
2106
|
}
|
2107
|
+
|
2108
|
+
get datasetVariables() {
|
2109
|
+
// Returns list with all ChartVariable objects in this model that
|
2110
|
+
// reference a regular dataset, i.e., not an equation
|
2111
|
+
const vl = [];
|
2112
|
+
for(let i = 0; i < MODEL.charts.length; i++) {
|
2113
|
+
const c = MODEL.charts[i];
|
2114
|
+
for(let j = 0; j < c.variables.length; j++) {
|
2115
|
+
const v = c.variables[j];
|
2116
|
+
if(v.object instanceof Dataset &&
|
2117
|
+
v.object !== MODEL.equations_dataset) vl.push(v);
|
2118
|
+
}
|
2119
|
+
}
|
2120
|
+
return vl;
|
2121
|
+
}
|
2122
|
+
|
2123
|
+
get notesWithTags() {
|
2124
|
+
// Returns a list with all notes having tags [[...]] in this model
|
2125
|
+
const nl = [];
|
2126
|
+
for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
|
2127
|
+
const c = this.clusters[k];
|
2128
|
+
for(let i = 0; i < c.notes.length; i++) {
|
2129
|
+
const n = c.notes[i];
|
2130
|
+
if(n.tagList) nl.push(n);
|
2131
|
+
}
|
2132
|
+
}
|
2133
|
+
return nl;
|
2134
|
+
}
|
2029
2135
|
|
2030
2136
|
get allExpressions() {
|
2031
|
-
// Returns list of all Expression objects
|
2137
|
+
// Returns list of all Expression objects in this model
|
2032
2138
|
// NOTE: start with dataset expressions, so that when recompiling
|
2033
2139
|
// their `level-based` property is set before recompiling the
|
2034
2140
|
// other expressions
|
2035
|
-
const
|
2141
|
+
const xl = [];
|
2036
2142
|
for(let k in this.datasets) if(this.datasets.hasOwnProperty(k)) {
|
2037
2143
|
const ds = this.datasets[k];
|
2038
2144
|
// NOTE: dataset modifier expressions include the equations
|
2039
2145
|
for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
|
2040
|
-
|
2146
|
+
xl.push(ds.modifiers[m].expression);
|
2041
2147
|
}
|
2042
2148
|
}
|
2043
2149
|
for(let k in this.actors) if(this.actors.hasOwnProperty(k)) {
|
2044
|
-
|
2150
|
+
xl.push(this.actors[k].weight);
|
2045
2151
|
}
|
2046
2152
|
for(let k in this.processes) if(this.processes.hasOwnProperty(k)) {
|
2047
2153
|
const p = this.processes[k];
|
2048
|
-
|
2154
|
+
xl.push(p.lower_bound, p.upper_bound, p.initial_level, p.pace_expression);
|
2049
2155
|
}
|
2050
2156
|
for(let k in this.products) if(this.products.hasOwnProperty(k)) {
|
2051
2157
|
const p = this.products[k];
|
2052
|
-
|
2158
|
+
xl.push(p.lower_bound, p.upper_bound, p.initial_level, p.price);
|
2053
2159
|
}
|
2054
2160
|
for(let k in this.clusters) if(this.clusters.hasOwnProperty(k)) {
|
2055
2161
|
const c = this.clusters[k];
|
2056
2162
|
for(let i = 0; i < c.notes.length; i++) {
|
2057
2163
|
const n = c.notes[i];
|
2058
|
-
|
2164
|
+
xl.push(n.color);
|
2059
2165
|
}
|
2060
2166
|
}
|
2061
2167
|
for(let k in this.links) if(this.links.hasOwnProperty(k)) {
|
2062
2168
|
const l = this.links[k];
|
2063
|
-
|
2169
|
+
xl.push(l.relative_rate, l.flow_delay);
|
2064
2170
|
}
|
2065
|
-
return
|
2171
|
+
return xl;
|
2066
2172
|
}
|
2067
2173
|
|
2068
|
-
replaceEntityInExpressions(en1, en2) {
|
2174
|
+
replaceEntityInExpressions(en1, en2, notify=true) {
|
2069
2175
|
// Replace entity name `en1` by `en2` in all variables in all expressions
|
2070
2176
|
// (provided that they are not identical)
|
2071
2177
|
if(en1 === en2) return;
|
@@ -2091,8 +2197,12 @@ console.log('HERE', d);
|
|
2091
2197
|
}
|
2092
2198
|
}
|
2093
2199
|
if(ioc.replace_count) {
|
2094
|
-
|
2095
|
-
|
2200
|
+
this.variable_count += ioc.replace_count;
|
2201
|
+
this.expression_count += ioc.expression_count;
|
2202
|
+
if(notify) {
|
2203
|
+
UI.notify(`Renamed ${pluralS(ioc.replace_count, 'variable')} in ` +
|
2204
|
+
pluralS(ioc.expression_count, 'expression'));
|
2205
|
+
}
|
2096
2206
|
}
|
2097
2207
|
// Also rename entities in parameters and outcomes of sensitivity analysis
|
2098
2208
|
for(let i = 0; i < this.sensitivity_parameters.length; i++) {
|
@@ -2127,8 +2237,8 @@ console.log('HERE', d);
|
|
2127
2237
|
const
|
2128
2238
|
en = escapeRegex(ena[0].trim().replace(/\s+/g, ' ').toLowerCase()),
|
2129
2239
|
at = ena[1].trim(),
|
2130
|
-
raw = en.replace(/\s/,
|
2131
|
-
re = new RegExp(`\[\s*${raw}\s*(\@[^\]]+)?\s*\]`, 'gi');
|
2240
|
+
raw = en.replace(/\s/, '\\s+') + '\\s*\\|\\s*' + escapeRegex(at),
|
2241
|
+
re = new RegExp(String.raw`\[\s*${raw}\s*(\@[^\]]+)?\s*\]`, 'gi');
|
2132
2242
|
// Count replacements made
|
2133
2243
|
let n = 0;
|
2134
2244
|
// Iterate over all expressions
|
@@ -4472,12 +4582,17 @@ class Note extends ObjectWithXYWH {
|
|
4472
4582
|
}
|
4473
4583
|
}
|
4474
4584
|
|
4585
|
+
get tagList() {
|
4586
|
+
// Returns a list of matches for [[...]], or NULL if none
|
4587
|
+
return this.contents.match(/\[\[[^\]]+\]\]/g);
|
4588
|
+
}
|
4589
|
+
|
4475
4590
|
parseFields() {
|
4476
4591
|
// Fills the list of fields by parsing all [[...]] tags in the text
|
4477
4592
|
// NOTE: this does not affect the text itself; tags will be replaced
|
4478
4593
|
// by numerical values only when drawing the note
|
4479
4594
|
this.fields.length = 0;
|
4480
|
-
const tags = this.
|
4595
|
+
const tags = this.tagList;
|
4481
4596
|
if(tags) {
|
4482
4597
|
for(let i = 0; i < tags.length; i++) {
|
4483
4598
|
const
|
@@ -4584,7 +4699,7 @@ class Note extends ObjectWithXYWH {
|
|
4584
4699
|
// Return a list with names of entities used in fields
|
4585
4700
|
const
|
4586
4701
|
fel = [],
|
4587
|
-
tags = this.
|
4702
|
+
tags = this.tagList;
|
4588
4703
|
for(let i = 0; i < tags.length; i++) {
|
4589
4704
|
const
|
4590
4705
|
tag = tags[i],
|
@@ -4608,7 +4723,7 @@ class Note extends ObjectWithXYWH {
|
|
4608
4723
|
if(en1 === en2) return;
|
4609
4724
|
const
|
4610
4725
|
raw = en1.split(/\s+/).join('\\\\s+'),
|
4611
|
-
re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', '
|
4726
|
+
re = new RegExp('\\[\\[\\s*' + raw + '\\s*(\\->|\\||\\])', 'gi'),
|
4612
4727
|
tags = this.contents.match(re);
|
4613
4728
|
if(tags) {
|
4614
4729
|
for(let i = 0; i < tags.length; i++) {
|
@@ -4773,7 +4888,14 @@ class NodeBox extends ObjectWithXYWH {
|
|
4773
4888
|
get numberContext() {
|
4774
4889
|
// Returns the string to be used to evaluate #, so for clusters, processes
|
4775
4890
|
// and products this is the string of trailing digits (or empty if none)
|
4776
|
-
|
4891
|
+
// of the node name, or if that does not end with a number, the trailing
|
4892
|
+
// digits of the first prefix (from right to left) that does
|
4893
|
+
const sn = UI.prefixesAndName(this.name);
|
4894
|
+
let nc = endsWithDigits(sn.pop());
|
4895
|
+
while(!nc && sn.length > 0) {
|
4896
|
+
nc = endsWithDigits(sn.pop());
|
4897
|
+
}
|
4898
|
+
return nc;
|
4777
4899
|
}
|
4778
4900
|
|
4779
4901
|
rename(name, actor_name) {
|
@@ -7593,8 +7715,8 @@ class Link {
|
|
7593
7715
|
tn = this.from_node;
|
7594
7716
|
}
|
7595
7717
|
// Otherwise, the FROM node is checked first
|
7596
|
-
let nc =
|
7597
|
-
if(!nc) nc =
|
7718
|
+
let nc = fn.numberContext;
|
7719
|
+
if(!nc) nc = tn.numberContext;
|
7598
7720
|
return nc;
|
7599
7721
|
}
|
7600
7722
|
|
@@ -7847,7 +7969,12 @@ class Dataset {
|
|
7847
7969
|
|
7848
7970
|
get numberContext() {
|
7849
7971
|
// Returns the string to be used to evaluate # (empty string if undefined)
|
7850
|
-
|
7972
|
+
const sn = UI.prefixesAndName(this.name);
|
7973
|
+
let nc = endsWithDigits(sn.pop());
|
7974
|
+
while(!nc && sn.length > 0) {
|
7975
|
+
nc = endsWithDigits(sn.pop());
|
7976
|
+
}
|
7977
|
+
return nc;
|
7851
7978
|
}
|
7852
7979
|
|
7853
7980
|
get selectorList() {
|
@@ -7877,6 +8004,27 @@ class Dataset {
|
|
7877
8004
|
return true;
|
7878
8005
|
}
|
7879
8006
|
|
8007
|
+
get inferPrefixableModifiers() {
|
8008
|
+
// Returns list of dataset modifiers with expressions that do not
|
8009
|
+
// reference any variable and hence could probably better be represented
|
8010
|
+
// by a prefixed dataset having the expression value as its default
|
8011
|
+
const pml = [];
|
8012
|
+
if(this !== this.equations_dataset) {
|
8013
|
+
const sl = this.plainSelectors;
|
8014
|
+
for(let i = 0; i < sl.length; i++) {
|
8015
|
+
if(!MODEL.isDimensionSelector(sl[i])) {
|
8016
|
+
const
|
8017
|
+
m = this.modifiers[sl[i].toLowerCase()],
|
8018
|
+
x = m.expression;
|
8019
|
+
// Static expressions without variables can also be used
|
8020
|
+
// as dataset default value
|
8021
|
+
if(x.isStatic && x.text.indexOf('[') < 0) pml.push(m);
|
8022
|
+
}
|
8023
|
+
}
|
8024
|
+
}
|
8025
|
+
return pml;
|
8026
|
+
}
|
8027
|
+
|
7880
8028
|
get timeStepDuration() {
|
7881
8029
|
// Returns duration of 1 time step on the time scale of this dataset
|
7882
8030
|
return this.time_scale * VM.time_unit_values[this.time_unit];
|
@@ -8045,11 +8193,11 @@ class Dataset {
|
|
8045
8193
|
}
|
8046
8194
|
if(this.default_selector) {
|
8047
8195
|
// If no experiment (so "normal" run), use default selector if specified
|
8048
|
-
const dm = this.modifiers[this.default_selector];
|
8196
|
+
const dm = this.modifiers[UI.nameToID(this.default_selector)];
|
8049
8197
|
if(dm) return dm.expression;
|
8050
8198
|
// Exception should never occur, but check anyway and log it
|
8051
8199
|
console.log('WARNING: Dataset "' + this.name +
|
8052
|
-
`" has no default selector "${this.default_selector}"
|
8200
|
+
`" has no default selector "${this.default_selector}"`, this.modifiers);
|
8053
8201
|
}
|
8054
8202
|
// Fall-through: return vector instead of expression
|
8055
8203
|
return this.vector;
|
@@ -8176,15 +8324,17 @@ class Dataset {
|
|
8176
8324
|
}
|
8177
8325
|
}
|
8178
8326
|
const ds = xmlDecoded(nodeContentByTag(node, 'default-selector'));
|
8179
|
-
if(ds && !this.modifiers[ds]) {
|
8327
|
+
if(ds && !this.modifiers[UI.nameToID(ds)]) {
|
8180
8328
|
UI.warn(`Dataset <tt>${this.name}</tt> has no selector <tt>${ds}</tt>`);
|
8181
8329
|
} else {
|
8182
8330
|
this.default_selector = ds;
|
8183
8331
|
}
|
8184
8332
|
}
|
8185
8333
|
|
8186
|
-
rename(name) {
|
8334
|
+
rename(name, notify=true) {
|
8187
8335
|
// Change the name of this dataset
|
8336
|
+
// When `notify` is FALSE, notifications are suppressed while the
|
8337
|
+
// number of affected datasets and expressions are counted
|
8188
8338
|
// NOTE: prevent renaming the equations dataset (just in case...)
|
8189
8339
|
if(this === MODEL.equations_dataset) return;
|
8190
8340
|
name = UI.cleanName(name);
|
@@ -8204,7 +8354,7 @@ class Dataset {
|
|
8204
8354
|
this.name = name;
|
8205
8355
|
MODEL.datasets[new_id] = this;
|
8206
8356
|
if(old_id !== new_id) delete MODEL.datasets[old_id];
|
8207
|
-
MODEL.replaceEntityInExpressions(old_name, name);
|
8357
|
+
MODEL.replaceEntityInExpressions(old_name, name, notify);
|
8208
8358
|
return MODEL.datasets[new_id];
|
8209
8359
|
}
|
8210
8360
|
|
@@ -9677,7 +9827,7 @@ class ExperimentRunResult {
|
|
9677
9827
|
return ['<run-result', (this.x_variable ? ' x-variable="1"' : ''),
|
9678
9828
|
(this.was_ignored ? ' ignored="1"' : ''),
|
9679
9829
|
'><object-id>', xmlEncoded(this.object_id),
|
9680
|
-
'</object-id><attribute>', this.attribute,
|
9830
|
+
'</object-id><attribute>', xmlEncoded(this.attribute),
|
9681
9831
|
'</attribute><count>', this.N,
|
9682
9832
|
'</count><sum>', this.sum,
|
9683
9833
|
'</sum><mean>', this.mean,
|
@@ -9695,7 +9845,12 @@ class ExperimentRunResult {
|
|
9695
9845
|
this.x_variable = nodeParameterValue(node, 'x-variable') === '1';
|
9696
9846
|
this.was_ignored = nodeParameterValue(node, 'ignored') === '1';
|
9697
9847
|
this.object_id = xmlDecoded(nodeContentByTag(node, 'object-id'));
|
9698
|
-
|
9848
|
+
// NOTE: special check to guarantee upward compatibility to version
|
9849
|
+
// 1.3.0 and higher
|
9850
|
+
let attr = nodeContentByTag(node, 'attribute');
|
9851
|
+
if(this.object_id === UI.EQUATIONS_DATASET_ID &&
|
9852
|
+
!earlierVersion(MODEL.version, '1.3.0')) attr = xmlDecoded(attr);
|
9853
|
+
this.attribute = attr;
|
9699
9854
|
this.N = safeStrToInt(nodeContentByTag(node, 'count'));
|
9700
9855
|
this.sum = safeStrToFloat(nodeContentByTag(node, 'sum'));
|
9701
9856
|
this.mean = safeStrToFloat(nodeContentByTag(node, 'mean'));
|
@@ -10126,6 +10281,17 @@ class Experiment {
|
|
10126
10281
|
return index;
|
10127
10282
|
}
|
10128
10283
|
|
10284
|
+
isDimensionSelector(s) {
|
10285
|
+
// Returns TRUE if `s` is a dimension selector in this experiment
|
10286
|
+
for(let i = 0; i < this.dimensions.length; i++) {
|
10287
|
+
if(this.dimensions[i].indexOf(s) >= 0) return true;
|
10288
|
+
}
|
10289
|
+
if(this.settings_selectors.indexOf(s) >= 0) return true;
|
10290
|
+
if(this.combination_selectors.indexOf(s) >= 0) return true;
|
10291
|
+
if(this.actor_selectors.indexOf(s) >= 0) return true;
|
10292
|
+
return false;
|
10293
|
+
}
|
10294
|
+
|
10129
10295
|
get asXML() {
|
10130
10296
|
let d = '';
|
10131
10297
|
for(let i = 0; i < this.dimensions.length; i++) {
|
@@ -65,8 +65,11 @@ function pluralS(n, s, special='') {
|
|
65
65
|
function safeStrToFloat(str, val=0) {
|
66
66
|
// Returns numeric value of floating point string, interpreting both
|
67
67
|
// dot and comma as decimal point
|
68
|
-
// NOTE: returns default value val if str is empty, null or undefined
|
69
|
-
|
68
|
+
// NOTE: returns default value `val` if `str` is empty, null or undefined,
|
69
|
+
// or contains a character that is invalid in a number
|
70
|
+
if(!str || str.match(/[^0-9eE\.\,\+\-]/)) return val;
|
71
|
+
str = str.replace(',', '.');
|
72
|
+
const f = (str ? parseFloat(str) : val);
|
70
73
|
return (isNaN(f) ? val : f);
|
71
74
|
}
|
72
75
|
|
@@ -186,6 +189,20 @@ function ellipsedText(text, n=50, m=10) {
|
|
186
189
|
// Functions used when comparing two Linny-R models
|
187
190
|
//
|
188
191
|
|
192
|
+
function earlierVersion(v1, v2) {
|
193
|
+
// Compares two version numbers and returns TRUE iff `v1` is earlier
|
194
|
+
// than `v2`
|
195
|
+
v1 = v1.split('.');
|
196
|
+
v2 = v2.split('.');
|
197
|
+
for(let i = 0; i < Math.min(v1.length, v2.length); i++) {
|
198
|
+
// NOTE: for legacy JS models, the major version number evaluates as 0
|
199
|
+
if(safeStrToInt(v1[i]) < safeStrToInt(v2[i])) return true;
|
200
|
+
if(safeStrToInt(v1[i]) > safeStrToInt(v2[i])) return false;
|
201
|
+
}
|
202
|
+
// Fall-through: same version numbers => NOT earlier
|
203
|
+
return false;
|
204
|
+
}
|
205
|
+
|
189
206
|
function differences(a, b, props) {
|
190
207
|
// Compares values of properties (in list `props`) of entities `a` and `b`,
|
191
208
|
// and returns a "dictionary" object with differences
|
@@ -310,18 +327,34 @@ function patternList(str) {
|
|
310
327
|
|
311
328
|
function patternMatch(str, patterns) {
|
312
329
|
// Returns TRUE when `str` matches the &|^-pattern
|
330
|
+
// NOTE: if a pattern starts with equals sign = then `str` must
|
331
|
+
// equal the rest of the pattern to match; if it starts with a tilde
|
332
|
+
// ~ then `str` must start with the rest of the pattern to match
|
313
333
|
for(let i = 0; i < patterns.length; i++) {
|
314
334
|
const p = patterns[i];
|
315
|
-
let
|
335
|
+
let pm,
|
336
|
+
match = true;
|
316
337
|
for(let j = 0; j < p.plus.length; j++) {
|
317
|
-
|
338
|
+
pm = p.plus[j];
|
339
|
+
if(pm.startsWith('=')) {
|
340
|
+
match = match && str === pm.substring(1);
|
341
|
+
} else if(pm.startsWith('~')) {
|
342
|
+
match = match && str.startsWith(pm.substring(1));
|
343
|
+
} else {
|
344
|
+
match = match && str.indexOf(pm) >= 0;
|
345
|
+
}
|
318
346
|
}
|
319
347
|
for(let j = 0; j < p.min.length; j++) {
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
348
|
+
pm = p.min[j];
|
349
|
+
if(pm.startsWith('=')) {
|
350
|
+
match = match && str !== pm.substring(1);
|
351
|
+
} else if(pm.startsWith('~')) {
|
352
|
+
match = match && !str.startsWith(pm.substring(1));
|
353
|
+
} else {
|
354
|
+
match = match && str.indexOf(pm) < 0;
|
355
|
+
}
|
324
356
|
}
|
357
|
+
if(match) return true;
|
325
358
|
}
|
326
359
|
return false;
|
327
360
|
}
|
@@ -508,9 +541,9 @@ function childNodeByTag(node, tag) {
|
|
508
541
|
// Returns the XML child node of `node` having node name `tag`, or NULL if
|
509
542
|
// no such child node exists
|
510
543
|
let cn = null;
|
511
|
-
for (let i = 0; i < node.
|
512
|
-
if(node.
|
513
|
-
cn = node.
|
544
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
545
|
+
if(node.childNodes[i].tagName === tag) {
|
546
|
+
cn = node.childNodes[i];
|
514
547
|
break;
|
515
548
|
}
|
516
549
|
}
|
@@ -745,16 +778,21 @@ if(NODE) module.exports = {
|
|
745
778
|
pluralS: pluralS,
|
746
779
|
safeStrToFloat: safeStrToFloat,
|
747
780
|
safeStrToInt: safeStrToInt,
|
781
|
+
rangeToList: rangeToList,
|
748
782
|
dateToString: dateToString,
|
749
783
|
msecToTime: msecToTime,
|
750
784
|
uniformDecimals: uniformDecimals,
|
751
785
|
ellipsedText: ellipsedText,
|
786
|
+
earlierVersion: earlierVersion,
|
752
787
|
differences: differences,
|
753
788
|
markFirstDifference: markFirstDifference,
|
789
|
+
ciCompare: ciCompare,
|
754
790
|
endsWithDigits: endsWithDigits,
|
755
791
|
indexOfMatchingBracket: indexOfMatchingBracket,
|
756
792
|
patternList: patternList,
|
757
793
|
patternMatch: patternMatch,
|
794
|
+
compareSelectors: compareSelectors,
|
795
|
+
stringToFloatArray: stringToFloatArray,
|
758
796
|
escapeRegex: escapeRegex,
|
759
797
|
addDistinct: addDistinct,
|
760
798
|
setString: setString,
|