linny-r 1.4.0 → 1.4.2
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 +1 -1
- package/server.js +48 -6
- package/static/index.html +9 -5
- package/static/linny-r.css +0 -14
- package/static/scripts/linny-r-ctrl.js +69 -4
- package/static/scripts/linny-r-gui.js +37 -26
- package/static/scripts/linny-r-model.js +136 -71
- package/static/scripts/linny-r-utils.js +38 -15
- package/static/scripts/linny-r-vm.js +265 -169
@@ -96,6 +96,7 @@ class LinnyRModel {
|
|
96
96
|
this.grid_pixels = 20;
|
97
97
|
this.align_to_grid = true;
|
98
98
|
this.infer_cost_prices = false;
|
99
|
+
this.report_results = false;
|
99
100
|
this.show_block_arrows = true;
|
100
101
|
this.last_zoom_factor = 1;
|
101
102
|
|
@@ -2486,6 +2487,7 @@ class LinnyRModel {
|
|
2486
2487
|
this.decimal_comma = nodeParameterValue(node, 'decimal-comma') === '1';
|
2487
2488
|
this.align_to_grid = nodeParameterValue(node, 'align-to-grid') === '1';
|
2488
2489
|
this.infer_cost_prices = nodeParameterValue(node, 'cost-prices') === '1';
|
2490
|
+
this.report_results = nodeParameterValue(node, 'report-results') === '1';
|
2489
2491
|
this.show_block_arrows = nodeParameterValue(node, 'block-arrows') === '1';
|
2490
2492
|
this.name = xmlDecoded(nodeContentByTag(node, 'name'));
|
2491
2493
|
this.author = xmlDecoded(nodeContentByTag(node, 'author'));
|
@@ -2828,6 +2830,7 @@ class LinnyRModel {
|
|
2828
2830
|
if(this.decimal_comma) p += ' decimal-comma="1"';
|
2829
2831
|
if(this.align_to_grid) p += ' align-to-grid="1"';
|
2830
2832
|
if(this.infer_cost_prices) p += ' cost-prices="1"';
|
2833
|
+
if(this.report_results) p += ' report-results="1"';
|
2831
2834
|
if(this.show_block_arrows) p += ' block-arrows="1"';
|
2832
2835
|
let xml = this.xml_header + ['<model', p, '><name>', xmlEncoded(this.name),
|
2833
2836
|
'</name><author>', xmlEncoded(this.author),
|
@@ -2937,24 +2940,72 @@ class LinnyRModel {
|
|
2937
2940
|
}
|
2938
2941
|
|
2939
2942
|
get outputData() {
|
2940
|
-
// Returns model results [data, statistics] in tab-separated format
|
2941
|
-
|
2942
|
-
|
2943
|
+
// Returns model results [data, statistics] in tab-separated format.
|
2944
|
+
const
|
2945
|
+
vbls = [],
|
2946
|
+
names = [],
|
2947
|
+
scale_re = /\s+\(x[0-9\.\,]+\)$/;
|
2948
|
+
// First create list of distinct variables used in charts.
|
2949
|
+
// NOTE: Also include those that are not "visible" in a chart.
|
2943
2950
|
for(let i = 0; i < this.charts.length; i++) {
|
2944
2951
|
const c = this.charts[i];
|
2945
2952
|
for(let j = 0; j < c.variables.length; j++) {
|
2946
|
-
|
2953
|
+
let v = c.variables[j],
|
2954
|
+
vn = v.displayName;
|
2955
|
+
// If variable is scaled, do not include it as such, but include
|
2956
|
+
// a new unscaled chart variable.
|
2957
|
+
if(vn.match(scale_re)) {
|
2958
|
+
vn = vn.replace(scale_re, '');
|
2959
|
+
// Add only if (now unscaled) variable has not been added already.
|
2960
|
+
if(names.indexOf(vn) < 0) {
|
2961
|
+
// NOTE: Chart variable object is used ony as adummy, so NULL
|
2962
|
+
// can be used as its "owner chart".
|
2963
|
+
const cv = new ChartVariable(null);
|
2964
|
+
cv.setProperties(v.object, v.attribute, false, '#000000');
|
2965
|
+
vbls.push(cv);
|
2966
|
+
names.push(uvn);
|
2967
|
+
}
|
2968
|
+
} else if(names.indexOf(vn) < 0) {
|
2969
|
+
// Keep track of the dataset and dataset modifier variables,
|
2970
|
+
// so they will not be added in the next FOR loop.
|
2971
|
+
vbls.push(v);
|
2972
|
+
names.push(vn);
|
2973
|
+
}
|
2947
2974
|
}
|
2948
2975
|
}
|
2949
|
-
//
|
2976
|
+
// Add new variables for each outcome dataset and each equation that
|
2977
|
+
// is not a chart variable.
|
2978
|
+
for(let id in this.datasets) if(this.datasets.hasOwnProperty(id)) {
|
2979
|
+
const
|
2980
|
+
ds = this.datasets[id],
|
2981
|
+
eq = (ds === this.equations_dataset);
|
2982
|
+
if(ds.outcome || eq) {
|
2983
|
+
for(let ms in ds.modifiers) if(ds.modifiers.hasOwnProperty(ms)) {
|
2984
|
+
const
|
2985
|
+
dm = ds.modifiers[ms],
|
2986
|
+
n = dm.displayName;
|
2987
|
+
// Do not add if already in the list.
|
2988
|
+
if(names.indexOf(n) < 0) {
|
2989
|
+
// Here, too, NULL can be used as "owner chart".
|
2990
|
+
const cv = new ChartVariable(null);
|
2991
|
+
cv.setProperties(ds, dm.selector, false, '#000000');
|
2992
|
+
vbls.push(cv);
|
2993
|
+
}
|
2994
|
+
}
|
2995
|
+
}
|
2996
|
+
}
|
2997
|
+
// Sort variables by their name.
|
2998
|
+
vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
|
2999
|
+
// Create a new chart as dummy, so without adding it to this model.
|
2950
3000
|
const c = new Chart();
|
2951
3001
|
for(let i = 0; i < vbls.length; i++) {
|
2952
3002
|
const v = vbls[i];
|
2953
3003
|
c.addVariable(v.object.displayName, v.attribute);
|
2954
3004
|
}
|
2955
|
-
|
3005
|
+
// NOTE: Call `draw` with FALSE to prevent display in the chart manager.
|
3006
|
+
c.draw(false);
|
3007
|
+
// After drawing, all variables and their statistics have been computed.
|
2956
3008
|
return [c.dataAsString, c.statisticsAsString];
|
2957
|
-
// @@@TO DO: also add statistics on "outcome" datasets
|
2958
3009
|
}
|
2959
3010
|
|
2960
3011
|
get listOfAllSelectors() {
|
@@ -4738,7 +4789,7 @@ class Note extends ObjectWithXYWH {
|
|
4738
4789
|
get numberContext() {
|
4739
4790
|
// Returns the string to be used to evaluate #. For notes this is
|
4740
4791
|
// their note number if specified, otherwise the number context of a
|
4741
|
-
// nearby
|
4792
|
+
// nearby node, and otherwise the number context of their cluster.
|
4742
4793
|
let n = this.number;
|
4743
4794
|
if(n) return n;
|
4744
4795
|
n = this.nearbyNode;
|
@@ -5203,16 +5254,8 @@ class NodeBox extends ObjectWithXYWH {
|
|
5203
5254
|
|
5204
5255
|
get numberContext() {
|
5205
5256
|
// Returns the string to be used to evaluate #, so for clusters,
|
5206
|
-
// processes and products this is
|
5207
|
-
|
5208
|
-
// a number, the trailing digits of the first prefix (from right to
|
5209
|
-
// left) that does end on a number
|
5210
|
-
const sn = UI.prefixesAndName(this.name);
|
5211
|
-
let nc = endsWithDigits(sn.pop());
|
5212
|
-
while(!nc && sn.length > 0) {
|
5213
|
-
nc = endsWithDigits(sn.pop());
|
5214
|
-
}
|
5215
|
-
return nc;
|
5257
|
+
// processes and products this is their "tail number".
|
5258
|
+
return UI.tailNumber(this.name);
|
5216
5259
|
}
|
5217
5260
|
|
5218
5261
|
get similarNumberedEntities() {
|
@@ -5283,14 +5326,14 @@ class NodeBox extends ObjectWithXYWH {
|
|
5283
5326
|
MODEL.replaceEntityInExpressions(old_name, this.displayName);
|
5284
5327
|
MODEL.inferIgnoredEntities();
|
5285
5328
|
// NOTE: Renaming may affect the node's display size.
|
5286
|
-
if(this.resize())
|
5329
|
+
if(this.resize()) UI.drawSelection(MODEL);
|
5287
5330
|
// NOTE: Only TRUE indicates a successful (cosmetic) name change.
|
5288
5331
|
return true;
|
5289
5332
|
}
|
5290
5333
|
|
5291
5334
|
resize() {
|
5292
|
-
// Resizes this node; returns TRUE iff size has changed
|
5293
|
-
//
|
5335
|
+
// Resizes this node; returns TRUE iff size has changed.
|
5336
|
+
// Therefore, keep track of original width and height.
|
5294
5337
|
const
|
5295
5338
|
ow = this.width,
|
5296
5339
|
oh = this.height,
|
@@ -5346,12 +5389,13 @@ class NodeBox extends ObjectWithXYWH {
|
|
5346
5389
|
}
|
5347
5390
|
|
5348
5391
|
drawWithLinks() {
|
5349
|
-
// TO DO:
|
5392
|
+
// TO DO: Also draw relevant arrows when this is a cluster.
|
5350
5393
|
UI.drawObject(this);
|
5351
5394
|
if(this instanceof Cluster) return;
|
5352
|
-
//
|
5395
|
+
// Draw ALL arrows associated with this node.
|
5353
5396
|
const fc = this.cluster;
|
5354
|
-
|
5397
|
+
fc.categorizeEntities();
|
5398
|
+
// Make list of arrows that represent a link related to this node.
|
5355
5399
|
let a,
|
5356
5400
|
alist = [];
|
5357
5401
|
for(let j = 0; j < fc.arrows.length; j++) {
|
@@ -5367,7 +5411,7 @@ class NodeBox extends ObjectWithXYWH {
|
|
5367
5411
|
}
|
5368
5412
|
}
|
5369
5413
|
}
|
5370
|
-
//
|
5414
|
+
// Draw all arrows in this list.
|
5371
5415
|
for(let i = 0; i < alist.length; i++) UI.drawObject(alist[i]);
|
5372
5416
|
}
|
5373
5417
|
|
@@ -7328,7 +7372,7 @@ class Process extends Node {
|
|
7328
7372
|
// without comments or their (X, Y) position
|
7329
7373
|
n = MODEL.black_box_entities[id];
|
7330
7374
|
// `n` is just the name, so remove the actor name if it was added
|
7331
|
-
if(an) n = n.
|
7375
|
+
if(an) n = n.substring(0, n.lastIndexOf(an));
|
7332
7376
|
col = true;
|
7333
7377
|
cmnts = '';
|
7334
7378
|
x = 0;
|
@@ -8271,17 +8315,10 @@ class DatasetModifier {
|
|
8271
8315
|
// NOTE: If the selector contains wildcards, return "?" to indicate
|
8272
8316
|
// that the value of # cannot be inferred at compile time.
|
8273
8317
|
if(this.hasWildcards) return '?';
|
8274
|
-
// Otherwise,
|
8275
|
-
//
|
8276
|
-
//
|
8277
|
-
|
8278
|
-
let nc = endsWithDigits(sn.pop());
|
8279
|
-
while(!nc && sn.length > 0) {
|
8280
|
-
nc = endsWithDigits(sn.pop());
|
8281
|
-
}
|
8282
|
-
// NOTE: if the selector has no tail number, return the number context
|
8283
|
-
// of the dataset of this modifier.
|
8284
|
-
return nc || this.dataset.numberContext;
|
8318
|
+
// Otherwise, return the "tail number" of the selector, or if the
|
8319
|
+
// selector has no tail number, return the number context of the
|
8320
|
+
// dataset of this modifier.
|
8321
|
+
return UI.tailnumber(this.name) || this.dataset.numberContext;
|
8285
8322
|
}
|
8286
8323
|
|
8287
8324
|
match(s) {
|
@@ -8366,14 +8403,8 @@ class Dataset {
|
|
8366
8403
|
|
8367
8404
|
get numberContext() {
|
8368
8405
|
// Returns the string to be used to evaluate #
|
8369
|
-
// Like for nodes, this is the
|
8370
|
-
|
8371
|
-
const sn = UI.prefixesAndName(this.name);
|
8372
|
-
let nc = endsWithDigits(sn.pop());
|
8373
|
-
while(!nc && sn.length > 0) {
|
8374
|
-
nc = endsWithDigits(sn.pop());
|
8375
|
-
}
|
8376
|
-
return nc;
|
8406
|
+
// Like for nodes, this is the "tail number" of the dataset name.
|
8407
|
+
return UI.tailNumber(this.name);
|
8377
8408
|
}
|
8378
8409
|
|
8379
8410
|
get selectorList() {
|
@@ -8847,10 +8878,11 @@ class ChartVariable {
|
|
8847
8878
|
this.non_zero_tally = 0;
|
8848
8879
|
this.exceptions = 0;
|
8849
8880
|
this.bin_tallies = [];
|
8881
|
+
this.wildcard_index = false;
|
8850
8882
|
}
|
8851
8883
|
|
8852
8884
|
setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true) {
|
8853
|
-
// Sets the defining properties for this chart variable
|
8885
|
+
// Sets the defining properties for this chart variable.
|
8854
8886
|
this.object = obj;
|
8855
8887
|
this.attribute = attr;
|
8856
8888
|
this.stacked = stck;
|
@@ -8862,12 +8894,18 @@ class ChartVariable {
|
|
8862
8894
|
|
8863
8895
|
get displayName() {
|
8864
8896
|
// Returns the name of the Linny-R entity and its attribute, followed
|
8865
|
-
// by its scale factor unless it equals 1 (no scaling)
|
8897
|
+
// by its scale factor unless it equals 1 (no scaling).
|
8866
8898
|
const sf = (this.scale_factor === 1 ? '' :
|
8867
8899
|
` (x${VM.sig4Dig(this.scale_factor)})`);
|
8868
|
-
// NOTE:
|
8869
|
-
if(this.object === MODEL.equations_dataset)
|
8870
|
-
|
8900
|
+
// NOTE: Display name of equation is just the equations dataset modifier.
|
8901
|
+
if(this.object === MODEL.equations_dataset) {
|
8902
|
+
let eqn = this.attribute;
|
8903
|
+
if(this.wildcard_index !== false) {
|
8904
|
+
eqn = eqn.replace('??', this.wildcard_index);
|
8905
|
+
}
|
8906
|
+
return eqn + sf;
|
8907
|
+
}
|
8908
|
+
// NOTE: Do not display the vertical bar if no attribute is specified.
|
8871
8909
|
if(!this.attribute) return this.object.displayName + sf;
|
8872
8910
|
return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
|
8873
8911
|
}
|
@@ -8937,7 +8975,7 @@ class ChartVariable {
|
|
8937
8975
|
}
|
8938
8976
|
|
8939
8977
|
computeVector() {
|
8940
|
-
// Compute vector for this variable (using run results if specified)
|
8978
|
+
// Compute vector for this variable (using run results if specified).
|
8941
8979
|
let xrun = null,
|
8942
8980
|
rr = null,
|
8943
8981
|
ri = this.chart.run_index;
|
@@ -8952,10 +8990,10 @@ class ChartVariable {
|
|
8952
8990
|
this.vector.length = 0;
|
8953
8991
|
}
|
8954
8992
|
}
|
8955
|
-
// Compute vector and statistics only if vector is still empty
|
8993
|
+
// Compute vector and statistics only if vector is still empty.
|
8956
8994
|
if(this.vector.length > 0) return;
|
8957
|
-
// NOTE: expression vectors start at t = 0 with initial values that
|
8958
|
-
// not be included in statistics
|
8995
|
+
// NOTE: expression vectors start at t = 0 with initial values that
|
8996
|
+
// should not be included in statistics.
|
8959
8997
|
let v,
|
8960
8998
|
av = null,
|
8961
8999
|
t_end;
|
@@ -8966,22 +9004,23 @@ class ChartVariable {
|
|
8966
9004
|
this.non_zero_tally = 0;
|
8967
9005
|
this.exceptions = 0;
|
8968
9006
|
if(rr) {
|
8969
|
-
// Use run results (time scaled) as "actual vector" `av` for this
|
9007
|
+
// Use run results (time scaled) as "actual vector" `av` for this
|
9008
|
+
// variable.
|
8970
9009
|
const tsteps = Math.ceil(this.chart.time_horizon / this.chart.time_scale);
|
8971
9010
|
av = [];
|
8972
|
-
// NOTE:
|
9011
|
+
// NOTE: `scaleDataToVector` expects "pure" data, so slice off v[0].
|
8973
9012
|
VM.scaleDataToVector(rr.vector.slice(1), av, xrun.time_step_duration,
|
8974
9013
|
this.chart.time_scale, tsteps, 1);
|
8975
9014
|
t_end = tsteps;
|
8976
9015
|
} else {
|
8977
9016
|
// Get the variable's own value (number, vector or expression)
|
8978
|
-
// Special case: when an experiment is running, variables that
|
8979
|
-
// dataset with no explicit modifier must recompute the
|
8980
|
-
// current experiment run combination
|
9017
|
+
// Special case: when an experiment is running, variables that
|
9018
|
+
// depict a dataset with no explicit modifier must recompute the
|
9019
|
+
// vector using the current experiment run combination.
|
8981
9020
|
if(MODEL.running_experiment &&
|
8982
9021
|
this.object instanceof Dataset && !this.attribute) {
|
8983
9022
|
// Check if dataset modifiers match the combination of selectors
|
8984
|
-
// for the active run
|
9023
|
+
// for the active run.
|
8985
9024
|
const mm = this.object.matchingModifiers(
|
8986
9025
|
MODEL.running_experiment.activeCombination);
|
8987
9026
|
// If so, use the first (the list should contain at most 1 selector)
|
@@ -9000,21 +9039,24 @@ class ChartVariable {
|
|
9000
9039
|
}
|
9001
9040
|
t_end = MODEL.end_period - MODEL.start_period + 1;
|
9002
9041
|
}
|
9003
|
-
// NOTE: when a chart combines run results with dataset vectors, the
|
9004
|
-
// may be longer than the # of time steps displayed in the chart
|
9042
|
+
// NOTE: when a chart combines run results with dataset vectors, the
|
9043
|
+
// latter may be longer than the # of time steps displayed in the chart.
|
9005
9044
|
t_end = Math.min(t_end, this.chart.total_time_steps);
|
9006
9045
|
this.N = t_end;
|
9007
9046
|
for(let t = 0; t <= t_end; t++) {
|
9008
|
-
// Get the result, store it, and incorporate it in statistics
|
9047
|
+
// Get the result, store it, and incorporate it in statistics.
|
9009
9048
|
if(!av) {
|
9010
9049
|
// Undefined attribute => zero (no error)
|
9011
9050
|
v = 0;
|
9012
9051
|
} else if(Array.isArray(av)) {
|
9013
|
-
// Attribute value is a vector
|
9052
|
+
// Attribute value is a vector.
|
9053
|
+
// NOTE: This vector may be shorter than t; then use 0.
|
9014
9054
|
v = (t < av.length ? av[t] : 0);
|
9015
9055
|
} else if(av instanceof Expression) {
|
9016
|
-
// Attribute value is an expression
|
9017
|
-
|
9056
|
+
// Attribute value is an expression. If this chart variable has
|
9057
|
+
// its wildcard vector index set, evaluate the expression with
|
9058
|
+
// this index as context number.
|
9059
|
+
v = av.result(t, this.wildcard_index);
|
9018
9060
|
} else {
|
9019
9061
|
// Attribute value must be a number
|
9020
9062
|
v = av;
|
@@ -9183,7 +9225,7 @@ class Chart {
|
|
9183
9225
|
|
9184
9226
|
variableIndexByName(n) {
|
9185
9227
|
for(let i = 0; i < this.variables.length; i++) {
|
9186
|
-
if(this.variables[i].displayName
|
9228
|
+
if(this.variables[i].displayName === n) return i;
|
9187
9229
|
}
|
9188
9230
|
return -1;
|
9189
9231
|
}
|
@@ -9208,20 +9250,39 @@ class Chart {
|
|
9208
9250
|
if(n === UI.EQUATIONS_DATASET_NAME) {
|
9209
9251
|
// For equations only the attribute (modifier selector)
|
9210
9252
|
dn = a;
|
9253
|
+
n = a;
|
9211
9254
|
} else if(!a) {
|
9212
9255
|
// If no attribute specified (=> dataset) only the entity name
|
9213
9256
|
dn = n;
|
9214
9257
|
}
|
9215
9258
|
let vi = this.variableIndexByName(dn);
|
9216
9259
|
if(vi >= 0) return vi;
|
9217
|
-
//
|
9260
|
+
// Check whether name refers to a Linny-R entity defined by the model.
|
9218
9261
|
let obj = MODEL.objectByName(n);
|
9219
9262
|
if(obj === null) {
|
9220
9263
|
UI.warn(`Unknown entity "${n}"`);
|
9221
9264
|
return null;
|
9222
|
-
}
|
9223
|
-
|
9265
|
+
}
|
9266
|
+
const eq = obj instanceof DatasetModifier;
|
9267
|
+
if(!eq) {
|
9268
|
+
// No equation and no attribute specified? Then assume default.
|
9224
9269
|
if(a === '') a = obj.defaultAttribute;
|
9270
|
+
} else if(n.indexOf('??') >= 0) {
|
9271
|
+
// Special case: for wildcard equations, add dummy variables
|
9272
|
+
// for each vector in the wildcard vector set of the equation
|
9273
|
+
// expression.
|
9274
|
+
const
|
9275
|
+
vlist = [],
|
9276
|
+
clr = this.nextAvailableDefaultColor,
|
9277
|
+
indices = Object.keys(obj.expression.wildcard_vectors);
|
9278
|
+
for(let i = 0; i < indices.length; i++) {
|
9279
|
+
const v = new ChartVariable(this);
|
9280
|
+
v.setProperties(MODEL.equations_dataset, dn, false, clr);
|
9281
|
+
v.wildcard_index = parseInt(indices[i]);
|
9282
|
+
this.variables.push(v);
|
9283
|
+
vlist.push(v);
|
9284
|
+
}
|
9285
|
+
return vlist;
|
9225
9286
|
}
|
9226
9287
|
const v = new ChartVariable(this);
|
9227
9288
|
v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
|
@@ -9314,7 +9375,7 @@ class Chart {
|
|
9314
9375
|
return VM.sig2Dig(s / 8760) + 'y';
|
9315
9376
|
}
|
9316
9377
|
|
9317
|
-
draw() {
|
9378
|
+
draw(display=true) {
|
9318
9379
|
// NOTE: The SVG drawing area is fixed to be 500 pixels high, so that
|
9319
9380
|
// when saved as a file, it will (at 300 dpi) be about 2 inches high.
|
9320
9381
|
// Its width will equal its hight times the W/H-ratio of the chart
|
@@ -9489,6 +9550,10 @@ class Chart {
|
|
9489
9550
|
}
|
9490
9551
|
}
|
9491
9552
|
|
9553
|
+
// Now all vectors have been computed. If `display` is FALSE, this
|
9554
|
+
// indicates that data is used only to save model results.
|
9555
|
+
if(!display) return;
|
9556
|
+
|
9492
9557
|
// Define the bins when drawing as histogram
|
9493
9558
|
if(this.histogram) {
|
9494
9559
|
this.value_range = maxv - minv;
|
@@ -124,9 +124,9 @@ function msecToTime(msec) {
|
|
124
124
|
const ts = new Date(msec).toISOString().slice(11, -1).split('.');
|
125
125
|
let hms = ts[0], ms = ts[1];
|
126
126
|
// Trim zero hours and minutes
|
127
|
-
while(hms.startsWith('00:')) hms = hms.
|
127
|
+
while(hms.startsWith('00:')) hms = hms.substring(3);
|
128
128
|
// Trim leading zero on first number
|
129
|
-
if(hms.startsWith('00')) hms = hms.
|
129
|
+
if(hms.startsWith('00')) hms = hms.substring(1);
|
130
130
|
// Trim msec when minutes > 0
|
131
131
|
if(hms.indexOf(':') > 0) return hms;
|
132
132
|
// If < 1 second, return as milliseconds
|
@@ -135,6 +135,14 @@ function msecToTime(msec) {
|
|
135
135
|
return hms + '.' + ms.slice(0, 1) + ' sec';
|
136
136
|
}
|
137
137
|
|
138
|
+
function compactClockTime() {
|
139
|
+
// Returns current time (no date) in 6 digits hhmmss.
|
140
|
+
const d = new Date();
|
141
|
+
return d.getHours().toString().padStart(2, '0') +
|
142
|
+
d.getMinutes().toString().padStart(2, '0') +
|
143
|
+
d.getSeconds().toString().padStart(2, '0');
|
144
|
+
}
|
145
|
+
|
138
146
|
function uniformDecimals(data) {
|
139
147
|
// Formats the numbers in the array `data` so that they have uniform decimals
|
140
148
|
// NOTE: (1) this routine assumes that all number strings have sig4Dig format;
|
@@ -165,7 +173,7 @@ function uniformDecimals(data) {
|
|
165
173
|
ss = v.split('e');
|
166
174
|
x = ss[1];
|
167
175
|
if(x.length < maxe) {
|
168
|
-
x = x[0] + '0' + x.
|
176
|
+
x = x[0] + '0' + x.substring(1);
|
169
177
|
}
|
170
178
|
data[i] = ss[0] + 'e' + x;
|
171
179
|
} else if(maxi > 3) {
|
@@ -470,6 +478,21 @@ function matchingNumber(m, s) {
|
|
470
478
|
return (n == m ? n : false);
|
471
479
|
}
|
472
480
|
|
481
|
+
function compareWithTailNumbers(s1, s2) {
|
482
|
+
// Returns 0 on equal, an integer < 0 if `s1` comes before `s2`, and
|
483
|
+
// an integer > 0 if `s2` comes before `s1`.
|
484
|
+
if(s1 === s2) return 0;
|
485
|
+
let tn1 = endsWithDigits(s1),
|
486
|
+
tn2 = endsWithDigits(s2);
|
487
|
+
if(tn1) s1 = s1.slice(0, -tn1.length);
|
488
|
+
if(tn2) s2 = s2.slice(0, -tn2.length);
|
489
|
+
let c = ciCompare(s1, s2);
|
490
|
+
if(c !== 0 || !(tn1 || tn2)) return c;
|
491
|
+
if(tn1 && tn2) return parseInt(tn1) - parseInt(tn2);
|
492
|
+
if(tn2) return -1;
|
493
|
+
return 1;
|
494
|
+
}
|
495
|
+
|
473
496
|
function compareSelectors(s1, s2) {
|
474
497
|
// Dataset selectors comparison is case-insensitive, and puts wildcards
|
475
498
|
// last, where * comes later than ?
|
@@ -484,7 +507,7 @@ function compareSelectors(s1, s2) {
|
|
484
507
|
star2 = s2.indexOf('*');
|
485
508
|
if(star1 >= 0) {
|
486
509
|
if(star2 < 0) return 1;
|
487
|
-
return s1
|
510
|
+
return ciCompare(s1, s2);
|
488
511
|
}
|
489
512
|
if(star2 >= 0) return -1;
|
490
513
|
// Replace ? by | because | has a higher ASCII value than all other chars
|
@@ -517,7 +540,7 @@ function compareSelectors(s1, s2) {
|
|
517
540
|
while(i >= 0 && s_1[i] === '-') i--;
|
518
541
|
// If trailing minuses, replace by as many spaces and add an exclamation point
|
519
542
|
if(i < n - 1) {
|
520
|
-
s_1 = s_1.
|
543
|
+
s_1 = s_1.substring(0, i);
|
521
544
|
while(s_1.length < n) s_1 += ' ';
|
522
545
|
s_1 += '!';
|
523
546
|
}
|
@@ -526,7 +549,7 @@ function compareSelectors(s1, s2) {
|
|
526
549
|
i = n - 1;
|
527
550
|
while(i >= 0 && s_2[i] === '-') i--;
|
528
551
|
if(i < n - 1) {
|
529
|
-
s_2 = s_2.
|
552
|
+
s_2 = s_2.substring(0, i);
|
530
553
|
while(s_2.length < n) s_2 += ' ';
|
531
554
|
s_2 += '!';
|
532
555
|
}
|
@@ -627,13 +650,11 @@ function customizeXML(str) {
|
|
627
650
|
// for example to rename entities in one go -- USE WITH CARE!
|
628
651
|
// First modify `str` -- by default, do nothing
|
629
652
|
|
630
|
-
|
631
|
-
if(str.indexOf('<author>
|
632
|
-
str = str.replace(
|
633
|
-
'is-information="1" no-slack="1"><name>Line $1: switch<');
|
634
|
-
str = str.replace(/: zon/g, ': solar');
|
653
|
+
/*
|
654
|
+
if(str.indexOf('<author>XXX</author>') >= 0) {
|
655
|
+
str = str.replace(/<url>NL\/(\w+)\.csv<\/url>/g, '<url></url>');
|
635
656
|
}
|
636
|
-
|
657
|
+
*/
|
637
658
|
|
638
659
|
// Finally, return the modified string
|
639
660
|
return str;
|
@@ -808,8 +829,9 @@ function stringToFloatArray(s) {
|
|
808
829
|
a = [];
|
809
830
|
while(i <= s.length) {
|
810
831
|
const
|
811
|
-
h = s.
|
812
|
-
r = h.
|
832
|
+
h = s.substring(i - 8, i),
|
833
|
+
r = h.substring(6, 2) + h.substring(4, 2) +
|
834
|
+
h.substring(2, 2) + h.substring(0, 2);
|
813
835
|
a.push(hexToFloat(r));
|
814
836
|
i += 8;
|
815
837
|
}
|
@@ -824,7 +846,7 @@ function hexToBytes(hex) {
|
|
824
846
|
// Converts a hex string to a Uint8Array
|
825
847
|
const bytes = [];
|
826
848
|
for(let i = 0; i < hex.length; i += 2) {
|
827
|
-
bytes.push(parseInt(hex.
|
849
|
+
bytes.push(parseInt(hex.substring(i, i + 2), 16));
|
828
850
|
}
|
829
851
|
return new Uint8Array(bytes);
|
830
852
|
}
|
@@ -911,6 +933,7 @@ if(NODE) module.exports = {
|
|
911
933
|
rangeToList: rangeToList,
|
912
934
|
dateToString: dateToString,
|
913
935
|
msecToTime: msecToTime,
|
936
|
+
compactClockTime: compactClockTime,
|
914
937
|
uniformDecimals: uniformDecimals,
|
915
938
|
ellipsedText: ellipsedText,
|
916
939
|
earlierVersion: earlierVersion,
|