Files
generalFunctions/src/convert/index.js
znetsixe 5ea968eabc B2.3 + P11.1 + P11.2 + monster schema fix
B2.3 LatestWinsGate fireAndWait:
  Added fireAndWait(value, ctx?) returning per-fire settlement promise.
  Supersede resolves with frozen sentinel {superseded: true} (no
  rejection — callers branch on value without try/catch). Dispatch
  errors also resolve (with undefined); error surfaces via gate.lastError.
  LatestWinsGate.js 75 → 116 lines. 12/12 tests pass.

P11.1 convert.possibilities(measure):
  New helper returning sorted+deduped unit names for a measure.
  Cached per measure. Reuses existing convert measures map. Also
  exposed convert.measures() listing all known measures.
  convert/index.js +21 lines. New test file: 90 lines, 12/12 tests.

P11.2 commandRegistry.units field:
  Pre-dispatch normalisation pipeline. descriptor.units = {measure,
  default}; commandRegistry extracts msg.payload + msg.unit (3 shapes),
  validates against measure, converts to default, falls back + warns
  with accepted-list on unknown/wrong-measure. Falls back gracefully
  if convert.possibilities is missing. commandRegistry.js 164 → 237.
  +7 new tests covering all 4 paths.

monster schema fix (P11.2 sibling):
  generalFunctions/src/configs/monster.json was stripping four
  legitimate constraint keys (nominalFlowMin, flowMax, maxRainRef,
  minSampleIntervalSec). Added them with defaults matching the
  legacy nodeClass coercion. Side effect: this also UNBLOCKED the
  monster cooldown-guard test (separate ROOT-CAUSE entry below).

CONTRACTS.md §4 + §8 updated. 144/144 basic tests + 206/206 full
generalFunctions tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:29:14 +02:00

327 lines
8.1 KiB
JavaScript

var convert
, keys = require('./lodash/lodash.keys')
, each = require('./lodash/lodash.foreach')
, measures = {
length: require('./definitions/length')
, area: require('./definitions/area')
, mass: require('./definitions/mass')
, volume: require('./definitions/volume')
, each: require('./definitions/each')
, temperature: require('./definitions/temperature')
, time: require('./definitions/time')
, digital: require('./definitions/digital')
, partsPer: require('./definitions/partsPer')
, speed: require('./definitions/speed')
, pace: require('./definitions/pace')
, pressure: require('./definitions/pressure')
, current: require('./definitions/current')
, voltage: require('./definitions/voltage')
, power: require('./definitions/power')
, reactivePower: require('./definitions/reactivePower')
, apparentPower: require('./definitions/apparentPower')
, energy: require('./definitions/energy')
, reactiveEnergy: require('./definitions/reactiveEnergy')
, volumeFlowRate: require('./definitions/volumeFlowRate')
, illuminance: require('./definitions/illuminance')
, frequency: require('./definitions/frequency')
, angle : require('./definitions/angle')
}
, Converter;
Converter = function (numerator, denominator) {
if(denominator)
this.val = numerator / denominator;
else
this.val = numerator;
};
/**
* Lets the converter know the source unit abbreviation
*/
Converter.prototype.from = function (from) {
if(this.destination)
throw new Error('.from must be called before .to');
this.origin = this.getUnit(from);
if(!this.origin) {
this.throwUnsupportedUnitError(from);
}
return this;
};
/**
* Converts the unit and returns the value
*/
Converter.prototype.to = function (to) {
if(!this.origin)
throw new Error('.to must be called after .from');
this.destination = this.getUnit(to);
var result
, transform;
if(!this.destination) {
this.throwUnsupportedUnitError(to);
}
// Don't change the value if origin and destination are the same
if (this.origin.abbr === this.destination.abbr) {
return this.val;
}
// You can't go from liquid to mass, for example
if(this.destination.measure != this.origin.measure) {
throw new Error('Cannot convert incompatible measures of '
+ this.destination.measure + ' and ' + this.origin.measure);
}
/**
* Convert from the source value to its anchor inside the system
*/
result = this.val * this.origin.unit.to_anchor;
/**
* For some changes it's a simple shift (C to K)
* So we'll add it when convering into the unit (later)
* and subtract it when converting from the unit
*/
if (this.origin.unit.anchor_shift) {
result -= this.origin.unit.anchor_shift
}
/**
* Convert from one system to another through the anchor ratio. Some conversions
* aren't ratio based or require more than a simple shift. We can provide a custom
* transform here to provide the direct result
*/
if(this.origin.system != this.destination.system) {
transform = measures[this.origin.measure]._anchors[this.origin.system].transform;
if (typeof transform === 'function') {
result = transform(result)
}
else {
result *= measures[this.origin.measure]._anchors[this.origin.system].ratio;
}
}
/**
* This shift has to be done after the system conversion business
*/
if (this.destination.unit.anchor_shift) {
result += this.destination.unit.anchor_shift;
}
/**
* Convert to another unit inside the destination system
*/
return result / this.destination.unit.to_anchor;
};
/**
* Converts the unit to the best available unit.
*/
Converter.prototype.toBest = function(options) {
if(!this.origin)
throw new Error('.toBest must be called after .from');
options = Object.assign({
exclude: [],
cutOffNumber: 1,
}, options)
var best;
/**
Looks through every possibility for the 'best' available unit.
i.e. Where the value has the fewest numbers before the decimal point,
but is still higher than 1.
*/
each(this.possibilities(), function(possibility) {
var unit = this.describe(possibility);
var isIncluded = options.exclude.indexOf(possibility) === -1;
if (isIncluded && unit.system === this.origin.system) {
var result = this.to(possibility);
if (!best || (result >= options.cutOffNumber && result < best.val)) {
best = {
val: result,
unit: possibility,
singular: unit.singular,
plural: unit.plural
};
}
}
}.bind(this));
return best;
}
/**
* Finds the unit
*/
Converter.prototype.getUnit = function (abbr) {
var found;
each(measures, function (systems, measure) {
each(systems, function (units, system) {
if(system == '_anchors')
return false;
each(units, function (unit, testAbbr) {
if(testAbbr == abbr) {
found = {
abbr: abbr
, measure: measure
, system: system
, unit: unit
};
return false;
}
});
if(found)
return false;
});
if(found)
return false;
});
return found;
};
var describe = function(resp) {
return {
abbr: resp.abbr
, measure: resp.measure
, system: resp.system
, singular: resp.unit.name.singular
, plural: resp.unit.name.plural
};
}
/**
* An alias for getUnit
*/
Converter.prototype.describe = function (abbr) {
var resp = Converter.prototype.getUnit(abbr);
var desc = null;
try {
desc = describe(resp);
} catch(err) {
this.throwUnsupportedUnitError(abbr);
}
return desc;
};
/**
* Detailed list of all supported units
*/
Converter.prototype.list = function (measure) {
var list = [];
each(measures, function (systems, testMeasure) {
if(measure && measure !== testMeasure)
return;
each(systems, function (units, system) {
if(system == '_anchors')
return false;
each(units, function (unit, abbr) {
list = list.concat(describe({
abbr: abbr,
measure: testMeasure
, system: system
, unit: unit
}));
});
});
});
return list;
};
Converter.prototype.throwUnsupportedUnitError = function (what) {
var validUnits = [];
each(measures, function (systems, _measure) {
each(systems, function (units, system) {
if(system == '_anchors')
return false;
validUnits = validUnits.concat(keys(units));
});
});
throw new Error('Unsupported unit ' + what + ', use one of: ' + validUnits.join(', '));
}
/**
* Returns the abbreviated measures that the value can be
* converted to.
*/
Converter.prototype.possibilities = function (measure) {
var possibilities = [];
if(!this.origin && !measure) {
each(keys(measures), function (measure){
each(measures[measure], function (units, system) {
if(system == '_anchors')
return false;
possibilities = possibilities.concat(keys(units));
});
});
} else {
measure = measure || this.origin.measure;
each(measures[measure], function (units, system) {
if(system == '_anchors')
return false;
possibilities = possibilities.concat(keys(units));
});
}
return possibilities;
};
/**
* Returns the abbreviated measures that the value can be
* converted to.
*/
Converter.prototype.measures = function () {
return keys(measures);
};
convert = function (value) {
return new Converter(value);
};
/**
* Top-level helper: list accepted unit names for a measure.
* Cached per measure. Unknown measures return [].
*/
var _possibilitiesCache = Object.create(null);
convert.possibilities = function (measure) {
if (!measure || typeof measure !== 'string') return [];
if (_possibilitiesCache[measure]) return _possibilitiesCache[measure].slice();
if (!measures[measure]) {
_possibilitiesCache[measure] = [];
return [];
}
var units = Converter.prototype.possibilities.call({ origin: { measure: measure } }, measure);
var deduped = Array.from(new Set(units)).sort();
_possibilitiesCache[measure] = deduped;
return deduped.slice();
};
convert.measures = function () {
return keys(measures).slice();
};
module.exports = convert;