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.
@@ -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.split(UI.PREFIXER);
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 > 0 && c.length < sl.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.contents.match(/\[\[[^\]]+\]\]/g);
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 && d !== this.equations_dataset) {
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 x = [];
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
- x.push(ds.modifiers[m].expression);
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
- x.push(this.actors[k].weight);
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
- x.push(p.lower_bound, p.upper_bound, p.initial_level, p.pace_expression);
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
- x.push(p.lower_bound, p.upper_bound, p.initial_level, p.price);
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
- x.push(n.color);
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
- x.push(l.relative_rate, l.flow_delay);
2169
+ xl.push(l.relative_rate, l.flow_delay);
2064
2170
  }
2065
- return x;
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
- UI.notify(`Renamed ${pluralS(ioc.replace_count, 'variable')} in ` +
2095
- pluralS(ioc.expression_count, 'expression'));
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/, `\s+`) + `\s*\|\s*` + escapeRegex(at),
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.contents.match(/\[\[[^\]]+\]\]/g);
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.contents.match(/\[\[[^\]]+\]\]/g);
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*(\\->|\\||\\])', 'g'),
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
- return endsWithDigits(this.name);
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 = endsWithDigits(fn.name);
7597
- if(!nc) nc = endsWithDigits(tn.name);
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
- return endsWithDigits(this.name);
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
- this.attribute = nodeContentByTag(node, 'attribute');
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
- const f = (str ? parseFloat(str.replace(',', '.')) : val);
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 match = true;
335
+ let pm,
336
+ match = true;
316
337
  for(let j = 0; j < p.plus.length; j++) {
317
- match = match && str.indexOf(p.plus[j]) >= 0;
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
- match = match && str.indexOf(p.min[j]) < 0;
321
- }
322
- if(match) {
323
- return true;
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.children.length; i++) {
512
- if(node.children[i].tagName === tag) {
513
- cn = node.children[i];
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,