linny-r 1.1.11 → 1.1.13
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
package/server.js
CHANGED
@@ -1236,13 +1236,13 @@ function serveStaticFile(res, path) {
|
|
1236
1236
|
if(path.startsWith('/diagrams/')) {
|
1237
1237
|
// Serve diagrams from the (main)/user/diagrams/ sub-directory
|
1238
1238
|
logAction('Diagram: ' + path);
|
1239
|
-
path = '/user' + path;
|
1239
|
+
path = WORKING_DIRECTORY + '/user' + path;
|
1240
1240
|
} else {
|
1241
1241
|
// Other files from the (main)/static/ subdirectory
|
1242
1242
|
logAction('Static file: ' + path);
|
1243
|
-
path = '/static' + path;
|
1243
|
+
path = MODULE_DIRECTORY + '/static' + path;
|
1244
1244
|
}
|
1245
|
-
fs.readFile(
|
1245
|
+
fs.readFile(path, (err, data) => {
|
1246
1246
|
if(err) {
|
1247
1247
|
console.log(err);
|
1248
1248
|
res.writeHead(404);
|
@@ -1471,7 +1471,7 @@ class Paper {
|
|
1471
1471
|
// achieved by multiplying the "gap" being (lengths - heights)/2 by
|
1472
1472
|
// (1 - |dy/l|). NOTE: we re-use the values of `th` and `tw`
|
1473
1473
|
// computed in the previous block!
|
1474
|
-
shift += th/2;
|
1474
|
+
shift += th / 2;
|
1475
1475
|
s = VM.sig4Dig(luc.share_of_cost * 100) + '%';
|
1476
1476
|
bb = this.numberSize(s, 7);
|
1477
1477
|
const sgap = (tw + bb.width + 3 - th - bb.height) / 2;
|
@@ -1631,7 +1631,8 @@ class Paper {
|
|
1631
1631
|
if(cp <= VM.MINUS_INFINITY || cp >= VM.PLUS_INFINITY) {
|
1632
1632
|
s = VM.sig4Dig(cp);
|
1633
1633
|
} else if(Math.abs(cp) <= VM.SIG_DIF_FROM_ZERO) {
|
1634
|
-
|
1634
|
+
// DO not display CP when it is "propagated" NO_COST
|
1635
|
+
s = (cp === VM.NO_COST ? '' : '0');
|
1635
1636
|
} else {
|
1636
1637
|
// NOTE: use the absolute value of the flow, as cost is not affected by direction
|
1637
1638
|
s = VM.sig4Dig(Math.abs(af) * soc * cp);
|
@@ -168,12 +168,6 @@ class LinnyRModel {
|
|
168
168
|
return olist;
|
169
169
|
}
|
170
170
|
|
171
|
-
get legacyVersion() {
|
172
|
-
// Return TRUE if the model as it has been loaded was not saved by
|
173
|
-
// JavaScript Linny-R
|
174
|
-
return this.version.indexOf('JS-') < 0;
|
175
|
-
}
|
176
|
-
|
177
171
|
get newProcessCode() {
|
178
172
|
// Return the next unused process code
|
179
173
|
const n = this.next_process_number;
|
@@ -1991,6 +1985,15 @@ class LinnyRModel {
|
|
1991
1985
|
// Initialize a model from the XML tree with `node` as root
|
1992
1986
|
// NOTE: do NOT reset and initialize basic model properties when *including*
|
1993
1987
|
// a module into the current model
|
1988
|
+
// NOTE: obsolete XML nodes indicate: legacy Linny-R model
|
1989
|
+
const legacy_model = (nodeParameterValue(node, 'view-options') +
|
1990
|
+
nodeParameterValue(node, 'autosave') +
|
1991
|
+
nodeParameterValue(node, 'look-ahead') +
|
1992
|
+
nodeParameterValue(node, 'save-series') +
|
1993
|
+
nodeParameterValue(node, 'show-lp') +
|
1994
|
+
nodeParameterValue(node, 'optional-slack')).length > 0;
|
1995
|
+
// Flag to set when legacy time series data are added
|
1996
|
+
this.legacy_datasets = false;
|
1994
1997
|
if(!IO_CONTEXT) {
|
1995
1998
|
this.reset();
|
1996
1999
|
this.next_process_number = safeStrToInt(
|
@@ -2016,8 +2019,8 @@ class LinnyRModel {
|
|
2016
2019
|
this.timeout_period = Math.max(0,
|
2017
2020
|
safeStrToInt(nodeContentByTag(node, 'timeout-period')));
|
2018
2021
|
// Legacy models have tag "optimization-period" instead of "block-length"
|
2019
|
-
const bl_tag = (
|
2020
|
-
'optimization-period'
|
2022
|
+
const bl_tag = nodeContentByTag(node, 'block-length') ||
|
2023
|
+
nodeContentByTag(node, 'optimization-period');
|
2021
2024
|
this.block_length = Math.max(1,
|
2022
2025
|
safeStrToInt(nodeContentByTag(node, bl_tag)));
|
2023
2026
|
this.start_period = Math.max(1,
|
@@ -2174,7 +2177,7 @@ class LinnyRModel {
|
|
2174
2177
|
}
|
2175
2178
|
// Clear the default (empty) equations dataset, or it will block adding it
|
2176
2179
|
if(!IO_CONTEXT) {
|
2177
|
-
this.datasets = {};
|
2180
|
+
if(!this.legacy_datasets) this.datasets = {};
|
2178
2181
|
this.equations_dataset = null;
|
2179
2182
|
}
|
2180
2183
|
// NOTE: keep track of datasets that load from URL or file
|
@@ -2319,7 +2322,7 @@ class LinnyRModel {
|
|
2319
2322
|
// NOTE: links in legacy Linny-R models by default have 100% share-of-cost;
|
2320
2323
|
// to minimize conversion effort, set SoC for SINGLE links OUT of processes
|
2321
2324
|
// to 100%
|
2322
|
-
if(
|
2325
|
+
if(legacy_model) {
|
2323
2326
|
for(let l in this.links) if(this.links.hasOwnProperty(l)) {
|
2324
2327
|
l = this.links[l];
|
2325
2328
|
// NOTE: preserve non-zero SoC values, as these have been specified
|
@@ -2704,6 +2707,36 @@ class LinnyRModel {
|
|
2704
2707
|
links = [],
|
2705
2708
|
constraints = [],
|
2706
2709
|
can_calculate = true;
|
2710
|
+
const
|
2711
|
+
// NOTE: define local functions as constants
|
2712
|
+
costAffectingConstraints = (p) => {
|
2713
|
+
// Returns number of relevant contraints (see below) that
|
2714
|
+
// can affect the cost price of product or process `p`
|
2715
|
+
let n = 0;
|
2716
|
+
for(let i = 0; i < constraints.length; i++) {
|
2717
|
+
const c = constraints[i];
|
2718
|
+
if((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
|
2719
|
+
(c.from_node === p && c.soc_direction === VM.SOC_Y_X)) n++;
|
2720
|
+
}
|
2721
|
+
return n;
|
2722
|
+
},
|
2723
|
+
inputsFromProcesses = (p, t) => {
|
2724
|
+
// Returns a tuple {n, nosoc, nz} where n is the number of input links
|
2725
|
+
// from processes, nosoc the number of these that carry no cost,
|
2726
|
+
// and nz the number of links having actual flow > 0
|
2727
|
+
let tuple = {n: 0, nosoc: 0, nz: 0};
|
2728
|
+
for(let i = 0; i < p.inputs.length; i++) {
|
2729
|
+
const l = p.inputs[i];
|
2730
|
+
// NOTE: only process --> product links can carry cost
|
2731
|
+
if(l.from_node instanceof Process) {
|
2732
|
+
tuple.n++;
|
2733
|
+
if(l.share_of_cost === 0) tuple.nosoc++;
|
2734
|
+
if(l.actualFlow(t) > VM.NEAR_ZERO) tuple.nz++;
|
2735
|
+
}
|
2736
|
+
}
|
2737
|
+
return tuple;
|
2738
|
+
};
|
2739
|
+
|
2707
2740
|
// First scan constraints X --> Y: these must have SoC > 0 and moreover
|
2708
2741
|
// the level of both X and Y must be non-zero, or they transfer no cost
|
2709
2742
|
for(let k in this.constraints) if(this.constraints.hasOwnProperty(k) &&
|
@@ -2738,12 +2771,7 @@ class LinnyRModel {
|
|
2738
2771
|
break;
|
2739
2772
|
}
|
2740
2773
|
// Count constraints that affect CP of this process
|
2741
|
-
let n =
|
2742
|
-
for(let i = 0; i < constraints.length; i++) {
|
2743
|
-
const c = constraints[i];
|
2744
|
-
if((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
|
2745
|
-
(c.from_node === p && c.soc_direction === VM.SOC_Y_X)) n++;
|
2746
|
-
}
|
2774
|
+
let n = costAffectingConstraints(p);
|
2747
2775
|
if(n || p.inputs.length) {
|
2748
2776
|
// All inputs can affect the CP of a process
|
2749
2777
|
p.cost_price[t] = VM.UNDEFINED;
|
@@ -2762,65 +2790,38 @@ class LinnyRModel {
|
|
2762
2790
|
if(pr < 0) negpr -= pr * l.relative_rate.result(dt);
|
2763
2791
|
}
|
2764
2792
|
p.cost_price[t] = negpr;
|
2793
|
+
// Done, so not add to `processes` list
|
2765
2794
|
}
|
2766
2795
|
}
|
2767
2796
|
// Then scan the products
|
2768
2797
|
for(let k in this.products) if(this.products.hasOwnProperty(k) &&
|
2769
2798
|
!MODEL.ignored_entities[k]) {
|
2770
2799
|
const p = this.products[k];
|
2771
|
-
let
|
2772
|
-
nc =
|
2773
|
-
|
2774
|
-
|
2775
|
-
|
2776
|
-
const l = p.inputs[i];
|
2777
|
-
// NOTE: only process --> product links can carry cost
|
2778
|
-
if(l.share_of_cost > 0 && l.from_node instanceof Process) n++;
|
2779
|
-
}
|
2780
|
-
if(p.is_buffer) {
|
2781
|
-
// Stocks often introduce cycles; those having only zero-flow
|
2782
|
-
// links in/out have stockprice of t-1
|
2800
|
+
let ifp = inputsFromProcesses(p, t),
|
2801
|
+
nc = costAffectingConstraints(p);
|
2802
|
+
if(p.is_buffer && !ifp.nz) {
|
2803
|
+
// Stocks for which all INput links have flow = 0 have the same
|
2804
|
+
// stock price as in t-1
|
2783
2805
|
// NOTE: it is not good to check for zero stock, as that may be
|
2784
2806
|
// the net result of in/outflows
|
2785
|
-
|
2786
|
-
|
2787
|
-
|
2788
|
-
|
2789
|
-
|
2790
|
-
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2794
|
-
|
2795
|
-
}
|
2796
|
-
} else if(n > 1) {
|
2797
|
-
// NOTE: products having no storage, and *multiple* cost-carrying
|
2798
|
-
// input links that all are zero-flow have CP=0
|
2799
|
-
let nz = 0;
|
2800
|
-
for(let i = 0; i < p.inputs.length && !nz; i++) {
|
2801
|
-
if(p.inputs[i].actualFlow(t) > VM.NEAR_ZERO) nz++;
|
2802
|
-
}
|
2803
|
-
if(!nz) n = 0;
|
2804
|
-
}
|
2805
|
-
// Add number of cost-transferring constraints
|
2806
|
-
for(let i = 0; i < constraints.length; i++) {
|
2807
|
-
const c = constraints[i];
|
2808
|
-
if(c.to_node === p && c.soc_direction === VM.SOC_X_Y ||
|
2809
|
-
(c.from_node === p && c.soc_direction === VM.SOC_Y_X)) nc++;
|
2810
|
-
}
|
2811
|
-
if(n + nc) {
|
2807
|
+
p.cost_price[t] = p.stockPrice(t - 1);
|
2808
|
+
p.stock_price[t] = p.cost_price[t];
|
2809
|
+
} else if(!nc && (ifp.n === ifp.nosoc || (!ifp.nz && ifp.n > ifp.nosoc + 1))) {
|
2810
|
+
// For products having only input links that carry no cost,
|
2811
|
+
// CP = 0 but coded as NO_COST so that this can propagate.
|
2812
|
+
// Furthermore, for products having no storage and *multiple*
|
2813
|
+
// cost-carrying input links that all are zero-flow, the cost
|
2814
|
+
// price cannot be inferred unambiguously => set to 0
|
2815
|
+
p.cost_price[t] = (ifp.n && ifp.n === ifp.nosoc ? VM.NO_COST : 0);
|
2816
|
+
} else {
|
2812
2817
|
// Cost price must be calculated
|
2813
2818
|
p.cost_price[t] = VM.UNDEFINED;
|
2814
|
-
p.stock_price[t] = VM.UNDEFINED;
|
2815
2819
|
products.push(p);
|
2816
|
-
} else {
|
2817
|
-
// Cost price is zero (for stocks: CP[t-1])
|
2818
|
-
p.cost_price[t] = cp;
|
2819
|
-
p.stock_price[t] = cp;
|
2820
2820
|
}
|
2821
|
+
p.cost_price[t] = p.cost_price[t];
|
2821
2822
|
}
|
2822
|
-
// Finally, scan all links, and
|
2823
|
-
//
|
2823
|
+
// Finally, scan all links, and retain only those for which the CP
|
2824
|
+
// can not already be inferred from their FROM node
|
2824
2825
|
for(let k in this.links) if(this.links.hasOwnProperty(k) &&
|
2825
2826
|
!MODEL.ignored_entities[k]) {
|
2826
2827
|
const
|
@@ -2828,15 +2829,8 @@ class LinnyRModel {
|
|
2828
2829
|
ld = l.actualDelay(t),
|
2829
2830
|
fn = l.from_node,
|
2830
2831
|
fncp = fn.costPrice(t - ld),
|
2831
|
-
tn = l.to_node
|
2832
|
-
|
2833
|
-
if(fncp !== VM.UNDEFINED && fncp !== VM.NOT_COMPUTED) {
|
2834
|
-
// Links that are output of a node having CP defined have UCP = CP
|
2835
|
-
l.unit_cost_price = fncp;
|
2836
|
-
} else if(tncp !== VM.UNDEFINED && tncp !== VM.NOT_COMPUTED) {
|
2837
|
-
// Links that are input of a node having CP defined have UCP = CP
|
2838
|
-
l.unit_cost_price = 0;
|
2839
|
-
} else if(fn instanceof Product && fn.price.defined) {
|
2832
|
+
tn = l.to_node;
|
2833
|
+
if(fn instanceof Product && fn.price.defined) {
|
2840
2834
|
// Links from products having a market price have this price
|
2841
2835
|
// multiplied by their relative rate as unit CP
|
2842
2836
|
l.unit_cost_price = fn.price.result(t) * l.relative_rate.result(t);
|
@@ -2845,6 +2839,9 @@ class LinnyRModel {
|
|
2845
2839
|
// Process output links that do not carry cost and product-to-
|
2846
2840
|
// product links have unit CP = 0
|
2847
2841
|
l.unit_cost_price = 0;
|
2842
|
+
} else if(fncp !== VM.UNDEFINED && fncp !== VM.NOT_COMPUTED) {
|
2843
|
+
// Links that are output of a node having CP defined have UCP = CP
|
2844
|
+
l.unit_cost_price = fncp * l.relative_rate.result(t);
|
2848
2845
|
} else {
|
2849
2846
|
l.unit_cost_price = VM.UNDEFINED;
|
2850
2847
|
// Do not push links related to processes having level < 0
|
@@ -2998,14 +2995,15 @@ class LinnyRModel {
|
|
2998
2995
|
cp_sccp = VM.COMPUTING;
|
2999
2996
|
for(let j = 0; j < p.inputs.length; j++) {
|
3000
2997
|
const l = p.inputs[j];
|
3001
|
-
if(l.from_node instanceof Process
|
2998
|
+
if(l.from_node instanceof Process) {
|
3002
2999
|
cp = l.from_node.costPrice(t - l.actualDelay(t));
|
3003
|
-
if(cp === VM.UNDEFINED) {
|
3000
|
+
if(cp === VM.UNDEFINED && l.share_of_cost > 0) {
|
3001
|
+
// Contibuting CP still unknown => break from FOR loop
|
3004
3002
|
break;
|
3005
3003
|
} else {
|
3006
3004
|
if(cp_sccp === VM.COMPUTING) {
|
3007
3005
|
// First CC process having a defined CP => use this CP
|
3008
|
-
cp_sccp = cp;
|
3006
|
+
cp_sccp = cp * l.share_of_cost;
|
3009
3007
|
} else {
|
3010
3008
|
// Multiple CC processes => set CP to 0
|
3011
3009
|
cp_sccp = 0;
|
@@ -3031,7 +3029,7 @@ class LinnyRModel {
|
|
3031
3029
|
if(cp === VM.UNDEFINED) continue;
|
3032
3030
|
// CP of product is 0 if no new production UNLESS it has only
|
3033
3031
|
// one cost-carrying production input, as then its CP equals
|
3034
|
-
// the CP of the producing process;
|
3032
|
+
// the CP of the producing process times the link SoC;
|
3035
3033
|
// if new production > 0 then CP = cost / quantity
|
3036
3034
|
if(cp_sccp !== VM.COMPUTING) {
|
3037
3035
|
cp = (qnp > 0 ? cnp / qnp : cp_sccp);
|
@@ -6461,11 +6459,14 @@ class Process extends Node {
|
|
6461
6459
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
6462
6460
|
this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
|
6463
6461
|
this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
|
6464
|
-
|
6465
|
-
|
6466
|
-
|
6467
|
-
|
6462
|
+
// NOTE: legacy models have no initial level field => default to 0
|
6463
|
+
const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
|
6464
|
+
this.initial_level.text = ilt || '0';
|
6465
|
+
// NOTE: until version 1.0.16, pace was stored as a node parameter;
|
6466
|
+
const pace_text = nodeParameterValue(node, 'pace') +
|
6468
6467
|
xmlDecoded(nodeContentByTag(node, 'pace'));
|
6468
|
+
// NOTE: legacy models have no pace field => default to 1
|
6469
|
+
this.pace_expression.text = pace_text || '1';
|
6469
6470
|
// NOTE: immediately evaluate pace expression as integer
|
6470
6471
|
this.pace = Math.max(1, Math.floor(this.pace_expression.result(1)));
|
6471
6472
|
this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
|
@@ -6871,15 +6872,44 @@ class Product extends Node {
|
|
6871
6872
|
this.integer_level = nodeParameterValue(node, 'integer-level') === '1';
|
6872
6873
|
this.no_slack = nodeParameterValue(node, 'no-slack') === '1';
|
6873
6874
|
// legacy models have tag "hidden" instead of "no-links"
|
6874
|
-
|
6875
|
-
|
6875
|
+
this.no_links = (nodeParameterValue(node, 'no-links') ||
|
6876
|
+
nodeParameterValue(node, 'hidden')) === '1';
|
6876
6877
|
this.scale_unit = MODEL.addScaleUnit(
|
6877
6878
|
xmlDecoded(nodeContentByTag(node, 'unit')));
|
6878
6879
|
// legacy models have tag "profit" instead of "price"
|
6879
|
-
|
6880
|
-
|
6880
|
+
let pp = nodeContentByTag(node, 'price');
|
6881
|
+
if(!pp) pp = nodeContentByTag(node, 'profit');
|
6882
|
+
this.price.text = xmlDecoded(pp);
|
6881
6883
|
this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
|
6882
6884
|
this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
|
6885
|
+
// legacy models can have LB and UB hexadecimal data strings
|
6886
|
+
const
|
6887
|
+
lb_data = nodeContentByTag(node, 'lower-bound-data'),
|
6888
|
+
ub_data = nodeContentByTag(node, 'upper-bound-data'),
|
6889
|
+
same = lb_data === ub_data;
|
6890
|
+
if(lb_data) {
|
6891
|
+
const
|
6892
|
+
dsn = this.displayName + (same ? '' : ' LOWER') + ' BOUND DATA',
|
6893
|
+
ds = MODEL.addDataset(dsn);
|
6894
|
+
ds.default_value = parseFloat(this.lower_bound.text);
|
6895
|
+
ds.data = stringToFloatArray(lb_data);
|
6896
|
+
ds.computeVector();
|
6897
|
+
ds.computeStatistics();
|
6898
|
+
this.lower_bound.text = `[${dsn}]`;
|
6899
|
+
if(same) this.equal_bounds = true;
|
6900
|
+
MODEL.legacy_datasets = true;
|
6901
|
+
}
|
6902
|
+
if(ub_data && !same) {
|
6903
|
+
const
|
6904
|
+
dsn = this.displayName + ' UPPER BOUND DATA',
|
6905
|
+
ds = MODEL.addDataset(dsn);
|
6906
|
+
ds.default_value = parseFloat(this.upper_bound.text);
|
6907
|
+
ds.data = stringToFloatArray(ub_data);
|
6908
|
+
ds.computeVector();
|
6909
|
+
ds.computeStatistics();
|
6910
|
+
this.upper_bound.text = `[${dsn}]`;
|
6911
|
+
MODEL.legacy_datasets = true;
|
6912
|
+
}
|
6883
6913
|
this.initial_level.text = xmlDecoded(
|
6884
6914
|
nodeContentByTag(node, 'initial-level'));
|
6885
6915
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
@@ -7187,12 +7217,16 @@ class Link {
|
|
7187
7217
|
this.is_feedback = nodeParameterValue(node, 'is-feedback') === '1';
|
7188
7218
|
this.relative_rate.text = xmlDecoded(
|
7189
7219
|
nodeContentByTag(node, 'relative-rate'));
|
7190
|
-
|
7191
|
-
|
7192
|
-
|
7220
|
+
// NOTE: legacy models have no flow delay field => default to 0
|
7221
|
+
const fd_text = xmlDecoded(nodeContentByTag(node, 'delay'));
|
7222
|
+
this.flow_delay.text = fd_text || '0';
|
7193
7223
|
this.share_of_cost = safeStrToFloat(
|
7194
7224
|
nodeContentByTag(node, 'share-of-cost'), 0);
|
7195
|
-
if(
|
7225
|
+
if(!fd_text) {
|
7226
|
+
// NOTE: default share-of-cost for links in legacy Linny-R was 100%;
|
7227
|
+
// this is dysfunctional in JS Linny-R => set to 0 if equal to 1
|
7228
|
+
if(this.share_of_cost == 1) this.share_of_cost = 0;
|
7229
|
+
}
|
7196
7230
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
7197
7231
|
if(IO_CONTEXT) {
|
7198
7232
|
// Record that this link was included
|
@@ -554,6 +554,32 @@ function nameToLines(name, actor_name = '') {
|
|
554
554
|
return lines.join('\n');
|
555
555
|
}
|
556
556
|
|
557
|
+
//
|
558
|
+
// Linny-R legacy model conversion functions
|
559
|
+
//
|
560
|
+
|
561
|
+
function hexToFloat(s) {
|
562
|
+
const n = parseInt('0x' + s, 16);
|
563
|
+
if(isNaN(n)) return 0;
|
564
|
+
const
|
565
|
+
sign = (n >> 31 ? -1 : 1),
|
566
|
+
exp = Math.pow(2, ((n >> 23) & 0xFF) - 127);
|
567
|
+
return sign * (n & 0x7fffff | 0x800000) * 1.0 / Math.pow(2, 23) * exp;
|
568
|
+
}
|
569
|
+
|
570
|
+
function stringToFloatArray(s) {
|
571
|
+
let i = 8,
|
572
|
+
a = [];
|
573
|
+
while(i <= s.length) {
|
574
|
+
const
|
575
|
+
h = s.substr(i - 8, 8),
|
576
|
+
r = h.substr(6, 2) + h.substr(4, 2) + h.substr(2, 2) + h.substr(0, 2);
|
577
|
+
a.push(hexToFloat(r));
|
578
|
+
i += 8;
|
579
|
+
}
|
580
|
+
return a;
|
581
|
+
}
|
582
|
+
|
557
583
|
//
|
558
584
|
// Encryption-related functions
|
559
585
|
//
|
@@ -1407,6 +1407,10 @@ class VirtualMachine {
|
|
1407
1407
|
// NOTE: below the "near zero" limit, a number is considered zero
|
1408
1408
|
// (this is to timely detect division-by-zero errors)
|
1409
1409
|
this.NEAR_ZERO = 1e-10;
|
1410
|
+
// Use a specific constant smaller than near-zero to denote "no cost"
|
1411
|
+
// to differentiate "no cost" form cost prices that really are 0
|
1412
|
+
this.NO_COST = 0.987654321e-10;
|
1413
|
+
|
1410
1414
|
// NOTE: allow for an accuracy margin: stocks may differ 0.1% from their
|
1411
1415
|
// target without displaying them in red or blue to signal shortage or surplus
|
1412
1416
|
this.SIG_DIF_LIMIT = 0.001;
|
@@ -1714,6 +1718,7 @@ class VirtualMachine {
|
|
1714
1718
|
if(n >= this.NOT_COMPUTED) return [true, '\u2297']; // Circled X
|
1715
1719
|
if(n >= this.UNDEFINED) return [true, '\u2047']; // Double question mark ??
|
1716
1720
|
if(n >= this.PLUS_INFINITY) return [true, '\u221E'];
|
1721
|
+
if(n === this.NO_COST) return [true, '\u00A2']; // c-slash (cent symbol)
|
1717
1722
|
return [false, n];
|
1718
1723
|
}
|
1719
1724
|
|