node-opcua-numeric-range
Version:
pure nodejs OPCUA SDK - module numeric-range
647 lines (646 loc) • 25.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NumericRange = exports.NumericRangeType = exports.schemaNumericRange = void 0;
exports.encodeNumericRange = encodeNumericRange;
exports.decodeNumericRange = decodeNumericRange;
/**
* @module node-opcua-numeric-range
*/
const node_opcua_assert_1 = require("node-opcua-assert");
const node_opcua_basic_types_1 = require("node-opcua-basic-types");
const node_opcua_factory_1 = require("node-opcua-factory");
const node_opcua_status_code_1 = require("node-opcua-status-code");
// OPC.UA Part 4 7.21 Numerical Range
// The syntax for the string contains one of the following two constructs. The first construct is the string
// representation of an individual integer. For example, '6' is valid, but '6.0' and '3.2' are not. The
// minimum and maximum values that can be expressed are defined by the use of this parameter and
// not by this parameter type definition. The second construct is a range represented by two integers
// separated by the colon (':') character. The first integer shall always have a lower value than the
// second. For example, '5:7' is valid, while '7:5' and '5:5' are not. The minimum and maximum values
// that can be expressed by these integers are defined by the use of this parameter , and not by this
// parameter type definition. No other characters, including white - space characters, are permitted.
// Multi- dimensional arrays can be indexed by specifying a range for each dimension separated by a ','.
//
// For example, a 2x2 block in a 4x4 matrix could be selected with the range '1:2,0:1'. A single element
// in a multi - dimensional array can be selected by specifying a single number instead of a range.
// For example, '1,1' specifies selects the [1,1] element in a two dimensional array.
// Dimensions are specified in the order that they appear in the ArrayDimensions Attribute.
// All dimensions shall be specified for a NumericRange to be valid.
//
// All indexes start with 0. The maximum value for any index is one less than the length of the
// dimension.
const NUMERIC_RANGE_EMPTY_STRING = "NumericRange:<Empty>";
// BNF of NumericRange
// The following BNF describes the syntax of the NumericRange parameter type.
// <numeric-range> ::= <dimension> [',' <dimension>]
// <dimension> ::= <index> [':' <index>]
// <index> ::= <digit> [<digit>]
// <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' |9'
//
// tslint:disable:object-literal-shorthand
// tslint:disable:only-arrow-functions
exports.schemaNumericRange = {
name: "NumericRange",
subType: "String",
defaultValue: () => {
return new NumericRange();
},
encode: encodeNumericRange,
decode: decodeNumericRange,
random: () => {
function r() {
return Math.ceil(Math.random() * 100);
}
const start = r();
const end = start + r();
return new NumericRange(start, end);
},
coerce: coerceNumericRange
};
(0, node_opcua_factory_1.registerBasicType)(exports.schemaNumericRange);
var NumericRangeType;
(function (NumericRangeType) {
NumericRangeType[NumericRangeType["Empty"] = 0] = "Empty";
NumericRangeType[NumericRangeType["SingleValue"] = 1] = "SingleValue";
NumericRangeType[NumericRangeType["ArrayRange"] = 2] = "ArrayRange";
NumericRangeType[NumericRangeType["MatrixRange"] = 3] = "MatrixRange";
NumericRangeType[NumericRangeType["InvalidRange"] = 4] = "InvalidRange";
})(NumericRangeType || (exports.NumericRangeType = NumericRangeType = {}));
// new Enum(["Empty", "SingleValue", "ArrayRange", "MatrixRange", "InvalidRange"]);
const regexNumericRange = /^[0-9:,]*$/;
function _valid_range(low, high) {
return !(low >= high || low < 0 || high < 0);
}
function construct_numeric_range_bit_from_string(str) {
const values = str.split(":");
if (values.length === 1) {
return {
type: NumericRangeType.SingleValue,
value: parseInt(values[0], 10)
};
}
else if (values.length === 2) {
const array = values.map((a) => parseInt(a, 10));
if (!_valid_range(array[0], array[1])) {
return {
type: NumericRangeType.InvalidRange,
value: str
};
}
return {
type: NumericRangeType.ArrayRange,
value: array
};
}
else {
return {
type: NumericRangeType.InvalidRange,
value: str
};
}
}
function _normalize(e) {
if (e.type === NumericRangeType.SingleValue) {
const ee = e;
return [ee.value, ee.value];
}
return e.value;
}
function construct_numeric_range_from_string(str) {
if (!regexNumericRange.test(str)) {
return {
type: NumericRangeType.InvalidRange,
value: str
};
}
/* detect multi dim range*/
const values = str.split(",");
if (values.length === 1) {
return construct_numeric_range_bit_from_string(values[0]);
}
else if (values.length === 2) {
const elements = values.map(construct_numeric_range_bit_from_string);
let rowRange = elements[0];
let colRange = elements[1];
if (rowRange.type === NumericRangeType.InvalidRange || colRange.type === NumericRangeType.InvalidRange) {
return { type: NumericRangeType.InvalidRange, value: str };
}
rowRange = _normalize(rowRange);
colRange = _normalize(colRange);
return {
type: NumericRangeType.MatrixRange,
value: [rowRange, colRange]
};
}
else {
// not supported yet
return { type: NumericRangeType.InvalidRange, value: str };
}
}
function construct_from_string(value) {
return construct_numeric_range_from_string(value);
}
function _set_single_value(value) {
if (value === null || value < 0 || !isFinite(value)) {
return {
type: NumericRangeType.InvalidRange,
value: "" + value?.toString()
};
}
else {
return {
type: NumericRangeType.SingleValue,
value: value
};
}
}
function _check_range(numericalRange) {
switch (numericalRange.type) {
case NumericRangeType.ArrayRange:
return _valid_range(numericalRange.value[0], numericalRange.value[1]);
}
// istanbul ignore next
throw new Error("unsupported case");
}
function _set_range_value(low, high) {
if (low === high) {
return {
type: NumericRangeType.SingleValue,
value: low
};
}
const numericalRange = {
type: NumericRangeType.ArrayRange,
value: [low, high]
};
if (!_check_range(numericalRange)) {
return {
type: NumericRangeType.InvalidRange,
value: ""
};
}
return numericalRange;
}
function construct_from_values(value, secondValue) {
if (secondValue === undefined) {
return _set_single_value(value);
}
else {
if (!isFinite(secondValue)) {
throw new Error(" invalid second argument, expecting a number");
}
return _set_range_value(value, secondValue);
}
}
function _construct_from_array(value, value2) {
(0, node_opcua_assert_1.assert)(value.length === 2);
// istanbul ignore next
if (!isFinite(value[0]) || !isFinite(value[1])) {
return { type: NumericRangeType.InvalidRange, value: "" + value };
}
let range1 = _set_range_value(value[0], value[1]);
if (!value2) {
return range1;
}
// we have a matrix
const nr2 = new NumericRange(value2);
// istanbul ignore next
if (nr2.type === NumericRangeType.InvalidRange ||
nr2.type === NumericRangeType.MatrixRange ||
nr2.type === NumericRangeType.Empty) {
return { type: NumericRangeType.InvalidRange, value: "" + value };
}
if (range1.type === NumericRangeType.SingleValue) {
range1 = {
type: NumericRangeType.ArrayRange,
value: [range1.value, range1.value]
};
}
if (nr2.type === NumericRangeType.SingleValue) {
nr2.type = NumericRangeType.ArrayRange;
nr2.value = [nr2.value, nr2.value];
}
// istanbul ignore next
return {
type: NumericRangeType.MatrixRange,
value: [range1.value, nr2.value]
};
}
class NumericRange {
static overlap(nr1, nr2) {
nr1 = nr1 || NumericRange.empty;
nr2 = nr2 || NumericRange.empty;
if (NumericRangeType.Empty === nr1.type || NumericRangeType.Empty === nr2.type) {
return true;
}
if (NumericRangeType.SingleValue === nr1.type && NumericRangeType.SingleValue === nr2.type) {
return nr1.value === nr2.value;
}
if (NumericRangeType.ArrayRange === nr1.type && NumericRangeType.ArrayRange === nr2.type) {
// +-----+ +------+ +---+ +------+
// +----+ +---+ +--------+ +---+
const l1 = nr1.value[0];
const h1 = nr1.value[1];
const l2 = nr2.value[0];
const h2 = nr2.value[1];
return _overlap(l1, h1, l2, h2);
}
// istanbul ignore next
(0, node_opcua_assert_1.assert)(false, "NumericalRange#overlap : case not implemented yet "); // TODO
// istanbul ignore next
return false;
}
constructor(value, secondValue) {
this.type = NumericRangeType.InvalidRange;
this.value = null;
(0, node_opcua_assert_1.assert)(!value || !(value instanceof NumericRange), "use coerce to create a NumericRange");
(0, node_opcua_assert_1.assert)(!secondValue || typeof secondValue === "number" || Array.isArray(secondValue));
if (typeof value === "string") {
const a = construct_from_string(value);
this.type = a.type;
this.value = a.value;
}
else if (typeof value === "number" &&
isFinite(value) &&
(secondValue === undefined || (typeof secondValue === "number" && isFinite(secondValue)))) {
const a = construct_from_values(value, secondValue);
this.type = a.type;
this.value = a.value;
}
else if (Array.isArray(value)) {
const a = _construct_from_array(value, secondValue);
this.type = a.type;
this.value = a.value;
}
else {
this.value = "<invalid>";
this.type = NumericRangeType.Empty;
}
// xx assert((this.type !== NumericRangeType.ArrayRange) || Array.isArray(this.value));
}
isValid() {
if (this.type === NumericRangeType.ArrayRange) {
const value = this.value;
if (value[0] < 0 || value[1] < 0) {
return false;
}
}
if (this.type === NumericRangeType.SingleValue) {
const value = this.value;
// istanbul ignore next
if (value < 0) {
return false;
}
}
return this.type !== NumericRangeType.InvalidRange;
}
isEmpty() {
return this.type === NumericRangeType.Empty;
}
isDefined() {
return this.type !== NumericRangeType.Empty && this.type !== NumericRangeType.InvalidRange;
}
toString() {
function array_range_to_string(values) {
(0, node_opcua_assert_1.assert)(Array.isArray(values));
if (values.length === 2 && values[0] === values[1]) {
return values[0].toString();
}
return values.map((value) => value.toString(10)).join(":");
}
function matrix_range_to_string(values) {
return values
.map((value) => {
return Array.isArray(value) ? array_range_to_string(value) : value.toString(10);
})
.join(",");
}
switch (this.type) {
case NumericRangeType.SingleValue:
return this.value.toString(10);
case NumericRangeType.ArrayRange:
return array_range_to_string(this.value);
case NumericRangeType.Empty:
return NUMERIC_RANGE_EMPTY_STRING;
case NumericRangeType.MatrixRange:
return matrix_range_to_string(this.value);
default:
(0, node_opcua_assert_1.assert)(this.type === NumericRangeType.InvalidRange);
return "NumericRange:<Invalid>";
}
}
toJSON() {
return this.toString();
}
toEncodeableString() {
switch (this.type) {
case NumericRangeType.SingleValue:
case NumericRangeType.ArrayRange:
case NumericRangeType.MatrixRange:
return this.toString();
case NumericRangeType.InvalidRange:
// istanbul ignore next
if (!(typeof this.value === "string")) {
throw new Error("Internal Error");
}
return this.value; // value contains the original strings which was detected invalid
default:
return null;
}
}
/**
* @param array flat array containing values or string
* @param dimensions: of the matrix if data is a matrix
* @return {*}
*/
extract_values(array, dimensions) {
const self = this;
if (!array) {
return {
array,
statusCode: this.type === NumericRangeType.Empty ? node_opcua_status_code_1.StatusCodes.Good : node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData
};
}
let index;
let low_index;
let high_index;
let rowRange;
let colRange;
switch (self.type) {
case NumericRangeType.Empty:
return extract_empty(array, dimensions);
case NumericRangeType.SingleValue:
index = self.value;
return extract_single_value(array, index);
case NumericRangeType.ArrayRange:
low_index = self.value[0];
high_index = self.value[1];
return extract_array_range(array, low_index, high_index);
case NumericRangeType.MatrixRange:
rowRange = self.value[0];
colRange = self.value[1];
return extract_matrix_range(array, rowRange, colRange, dimensions);
default:
return { statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeInvalid };
}
}
set_values_matrix(sourceToAlter, newMatrix) {
const { matrix, dimensions } = sourceToAlter;
const self = this;
(0, node_opcua_assert_1.assert)(dimensions, "expecting valid dimensions here");
if (self.type !== NumericRangeType.MatrixRange) {
// istanbul ignore next
return { matrix, statusCode: node_opcua_status_code_1.StatusCodes.BadTypeMismatch };
}
(0, node_opcua_assert_1.assert)(dimensions.length === 2);
const nbRows = dimensions[0];
const nbCols = dimensions[1];
(0, node_opcua_assert_1.assert)(sourceToAlter.matrix.length === nbRows * nbCols);
const [rowStart, rowEnd] = self.value[0];
const [colStart, colEnd] = self.value[1];
const nbRowInNew = rowEnd - rowStart + 1;
const nbColInNew = colEnd - colStart + 1;
if (nbRowInNew * nbColInNew !== newMatrix.length) {
return { matrix, statusCode: node_opcua_status_code_1.StatusCodes.BadTypeMismatch };
}
// check if the sub-matrix is in th range of the initial matrix
if (rowEnd >= nbRows || colEnd >= nbCols) {
// debugLog("out of band range => ", { rowEnd, nbRows, colEnd, nbCols });
return { matrix, statusCode: node_opcua_status_code_1.StatusCodes.BadTypeMismatch };
}
for (let row = rowStart; row <= rowEnd; row++) {
const ri = row - rowStart;
for (let col = colStart; col <= colEnd; col++) {
const ci = col - colStart;
matrix[row * nbCols + col] = newMatrix[ri * nbColInNew + ci];
}
}
return {
matrix,
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
}
set_values(arrayToAlter, newValues) {
assert_array_or_buffer(arrayToAlter);
assert_array_or_buffer(newValues);
const self = this;
let low_index;
let high_index;
switch (self.type) {
case NumericRangeType.Empty:
low_index = 0;
high_index = arrayToAlter.length - 1;
break;
case NumericRangeType.SingleValue:
low_index = self.value;
high_index = self.value;
break;
case NumericRangeType.ArrayRange:
low_index = self.value[0];
high_index = self.value[1];
break;
case NumericRangeType.MatrixRange:
// for the time being MatrixRange is not supported
return { array: arrayToAlter, statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData };
default:
return { array: null, statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeInvalid };
}
if (high_index >= arrayToAlter.length || low_index >= arrayToAlter.length) {
return { array: null, statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData };
}
if (this.type !== NumericRangeType.Empty && newValues.length !== high_index - low_index + 1) {
return { array: null, statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeInvalid };
}
const insertInPlace = Array.isArray(arrayToAlter)
? insertInPlaceStandardArray
: arrayToAlter instanceof Buffer
? insertInPlaceBuffer
: insertInPlaceTypedArray;
return {
array: insertInPlace(arrayToAlter, low_index, high_index, newValues),
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
}
}
exports.NumericRange = NumericRange;
NumericRange.coerce = coerceNumericRange;
NumericRange.schema = exports.schemaNumericRange;
// tslint:disable:variable-name
NumericRange.NumericRangeType = NumericRangeType;
NumericRange.empty = new NumericRange();
function slice(arr, start, end) {
if (start === 0 && end === arr.length) {
return arr;
}
let res;
if (arr.buffer instanceof ArrayBuffer) {
res = arr.subarray(start, end);
}
else if (arr instanceof Buffer) {
res = arr.subarray(start, end);
}
else {
(0, node_opcua_assert_1.assert)(typeof arr.slice === "function");
(0, node_opcua_assert_1.assert)(arr instanceof Buffer || arr instanceof Array || typeof arr === "string");
res = arr.slice(start, end);
}
if (res instanceof Uint8Array && arr instanceof Buffer) {
// note in io-js 3.00 onward standard Buffer are implemented differently and
// provides a buffer member and a subarray method, in fact in io-js 3.0
// it seems that Buffer acts as a Uint8Array. in this very special case
// we need to make sure that we end up with a Buffer object and not a Uint8Array.
res = Buffer.from(res);
}
return res;
}
function extract_empty(array, dimensions) {
return {
array: slice(array, 0, array.length),
dimensions,
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
}
function extract_single_value(array, index) {
if (index >= array.length) {
if (typeof array === "string") {
return { array: "", statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData };
}
return { array: null, statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData };
}
return {
array: slice(array, index, index + 1),
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
}
function extract_array_range(array, low_index, high_index) {
(0, node_opcua_assert_1.assert)(isFinite(low_index) && isFinite(high_index));
(0, node_opcua_assert_1.assert)(low_index >= 0);
(0, node_opcua_assert_1.assert)(low_index <= high_index);
if (low_index >= array.length) {
if (typeof array === "string") {
return { array: "", statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData };
}
return { array: null, statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData };
}
// clamp high index
high_index = Math.min(high_index, array.length - 1);
return {
array: slice(array, low_index, high_index + 1),
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
}
function isArrayLike(value) {
return typeof value.length === "number" || Object.prototype.hasOwnProperty.call(value, "length");
}
function extract_matrix_range(array, rowRange, colRange, dimension) {
(0, node_opcua_assert_1.assert)(Array.isArray(rowRange) && Array.isArray(colRange));
if (array.length === 0) {
return {
array: null,
statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData
};
}
if (isArrayLike(array[0]) && !dimension) {
// like extracting data from a one dimensional array of strings or byteStrings...
const result = extract_array_range(array, rowRange[0], rowRange[1]);
for (let i = 0; i < result.array.length; i++) {
const e = result.array[i];
result.array[i] = extract_array_range(e, colRange[0], colRange[1]).array;
}
return result;
}
if (!dimension) {
return {
array: null,
statusCode: node_opcua_status_code_1.StatusCodes.BadIndexRangeNoData
};
}
(0, node_opcua_assert_1.assert)(dimension, "expecting dimension to know the shape of the matrix represented by the flat array");
//
const rowLow = rowRange[0];
const rowHigh = rowRange[1];
const colLow = colRange[0];
const colHigh = colRange[1];
const nbRow = dimension[0];
const nbCol = dimension[1];
const nbRowDest = rowHigh - rowLow + 1;
const nbColDest = colHigh - colLow + 1;
// construct an array of the same type with the appropriate length to
// store the extracted matrix.
const tmp = new array.constructor(nbColDest * nbRowDest);
let row;
let col;
let r;
let c;
r = 0;
for (row = rowLow; row <= rowHigh; row++) {
c = 0;
for (col = colLow; col <= colHigh; col++) {
const srcIndex = row * nbCol + col;
const destIndex = r * nbColDest + c;
tmp[destIndex] = array[srcIndex];
c++;
}
r += 1;
}
return {
array: tmp,
dimensions: [nbRowDest, nbColDest],
statusCode: node_opcua_status_code_1.StatusCodes.Good
};
}
function assert_array_or_buffer(array) {
(0, node_opcua_assert_1.assert)(Array.isArray(array) || array.buffer instanceof ArrayBuffer || array instanceof Buffer);
}
function insertInPlaceStandardArray(arrayToAlter, low, high, newValues) {
const args = [low, high - low + 1].concat(newValues);
arrayToAlter.splice(...args);
return arrayToAlter;
}
function insertInPlaceTypedArray(arrayToAlter, low, high, newValues) {
if (low === 0 && high === arrayToAlter.length - 1) {
return new arrayToAlter.constructor(newValues);
}
(0, node_opcua_assert_1.assert)(newValues.length === high - low + 1);
arrayToAlter.subarray(low, high + 1).set(newValues);
return arrayToAlter;
}
function insertInPlaceBuffer(bufferToAlter, low, high, newValues) {
// insertInPlaceBuffer with buffer is not really possible as existing Buffer cannot be resized
if (!(bufferToAlter instanceof Buffer))
throw new Error("expecting a buffer");
if (low === 0 && high === bufferToAlter.length - 1) {
bufferToAlter = Buffer.from(newValues);
return bufferToAlter;
}
(0, node_opcua_assert_1.assert)(newValues.length === high - low + 1);
for (let i = 0; i < newValues.length; i++) {
bufferToAlter[i + low] = newValues[i];
}
return bufferToAlter;
}
function _overlap(l1, h1, l2, h2) {
return Math.max(l1, l2) <= Math.min(h1, h2);
}
function encodeNumericRange(numericRange, stream) {
(0, node_opcua_assert_1.assert)(numericRange instanceof NumericRange);
(0, node_opcua_basic_types_1.encodeString)(numericRange.toEncodeableString(), stream);
}
function decodeNumericRange(stream, _value) {
const str = (0, node_opcua_basic_types_1.decodeString)(stream);
return new NumericRange(str);
}
function coerceNumericRange(value) {
if (value instanceof NumericRange) {
return value;
}
if (value === null || value === undefined) {
return new NumericRange();
}
if (value === NUMERIC_RANGE_EMPTY_STRING) {
return new NumericRange();
}
(0, node_opcua_assert_1.assert)(typeof value === "string" || Array.isArray(value));
return new NumericRange(value);
}
//# sourceMappingURL=numeric_range.js.map