UNPKG

node-opcua-numeric-range

Version:

pure nodejs OPCUA SDK - module numeric-range

647 lines (646 loc) 25.5 kB
"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