node-opcua-numeric-range
Version:
pure nodejs OPCUA SDK - module numeric-range
782 lines (686 loc) • 27.2 kB
text/typescript
/**
* @module node-opcua-numeric-range
*/
import { assert } from "node-opcua-assert";
import { decodeString, encodeString, UAString } from "node-opcua-basic-types";
import { BinaryStream, OutputBinaryStream } from "node-opcua-binary-stream";
import { registerBasicType } from "node-opcua-factory";
import { StatusCode, StatusCodes } from "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
export const schemaNumericRange = {
name: "NumericRange",
subType: "String",
defaultValue: (): NumericRange => {
return new NumericRange();
},
encode: encodeNumericRange,
decode: decodeNumericRange,
random: (): NumericRange => {
function r() {
return Math.ceil(Math.random() * 100);
}
const start = r();
const end = start + r();
return new NumericRange(start, end);
},
coerce: coerceNumericRange
};
registerBasicType(schemaNumericRange);
export enum NumericRangeType {
Empty = 0,
SingleValue = 1,
ArrayRange = 2,
MatrixRange = 3,
InvalidRange = 4
}
// new Enum(["Empty", "SingleValue", "ArrayRange", "MatrixRange", "InvalidRange"]);
const regexNumericRange = /^[0-9:,]*$/;
function _valid_range(low: number, high: number): boolean {
return !(low >= high || low < 0 || high < 0);
}
type NumericalRangeValueType = null | number | string | number[] | number[][];
export interface NumericalRangeSingleValue {
type: NumericRangeType.SingleValue;
value: number;
}
export interface NumericalRangeArrayRange {
type: NumericRangeType.ArrayRange;
value: number[];
}
export interface NumericalRangeMatrixRange {
type: NumericRangeType.MatrixRange;
value: number[][];
}
export interface NumericalRangeEmpty {
type: NumericRangeType.Empty;
value: null;
}
export interface NumericalRangeInvalid {
type: NumericRangeType.InvalidRange;
value: string;
}
export type NumericalRange0 =
| NumericalRangeSingleValue
| NumericalRangeArrayRange
| NumericalRangeMatrixRange
| NumericalRangeEmpty
| NumericalRangeInvalid;
export interface NumericalRange1 {
type: NumericRangeType;
value: NumericalRangeValueType;
}
function construct_numeric_range_bit_from_string(str: string): NumericalRange0 {
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: NumericalRange1): number | number[] {
if (e.type === NumericRangeType.SingleValue) {
const ee = e as NumericalRangeSingleValue;
return [ee.value, ee.value];
}
return e.value as number;
}
function construct_numeric_range_from_string(str: string): NumericalRange0 {
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: any = elements[0];
let colRange: any = 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: string): NumericalRange0 {
return construct_numeric_range_from_string(value);
}
function _set_single_value(value: number | null): NumericalRange0 {
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: NumericalRange0) {
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: number, high: number): NumericalRangeSingleValue | NumericalRangeArrayRange | NumericalRangeInvalid {
if (low === high) {
return {
type: NumericRangeType.SingleValue,
value: low
};
}
const numericalRange: NumericalRangeArrayRange = {
type: NumericRangeType.ArrayRange,
value: [low, high]
};
if (!_check_range(numericalRange as NumericalRangeArrayRange)) {
return {
type: NumericRangeType.InvalidRange,
value: ""
};
}
return numericalRange;
}
function construct_from_values(value: number, secondValue?: number): NumericalRange0 {
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: number[], value2?: any): NumericalRange0 {
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 as number, nr2.value as number];
}
// istanbul ignore next
return {
type: NumericRangeType.MatrixRange,
value: [range1.value as number[], nr2.value as number[]]
};
}
export class NumericRange implements NumericalRange1 {
public static coerce = coerceNumericRange;
public static schema = schemaNumericRange;
// tslint:disable:variable-name
public static NumericRangeType = NumericRangeType;
public static readonly empty = new NumericRange() as NumericalRange0;
public static overlap(nr1?: NumericalRange0, nr2?: NumericalRange0): boolean {
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
assert(false, "NumericalRange#overlap : case not implemented yet "); // TODO
// istanbul ignore next
return false;
}
public type: NumericRangeType;
public value: NumericalRangeValueType;
constructor();
// tslint:disable-next-line: unified-signatures
constructor(value: string | null);
// tslint:disable-next-line: unified-signatures
constructor(value: number, secondValue?: number);
// tslint:disable-next-line: unified-signatures
constructor(value: number[]);
// tslint:disable-next-line: unified-signatures
constructor(value: number[], secondValue: number[]);
constructor(value?: null | string | number | number[], secondValue?: number | number[]) {
this.type = NumericRangeType.InvalidRange;
this.value = null;
assert(!value || !(value instanceof NumericRange), "use coerce to create a NumericRange");
assert(!secondValue || typeof secondValue === "number" || Array.isArray(secondValue));
if (typeof value === "string") {
const a = construct_from_string(value as string);
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));
}
public isValid(): boolean {
if (this.type === NumericRangeType.ArrayRange) {
const value = this.value as number[];
if (value[0] < 0 || value[1] < 0) {
return false;
}
}
if (this.type === NumericRangeType.SingleValue) {
const value = this.value as number;
// istanbul ignore next
if (value < 0) {
return false;
}
}
return this.type !== NumericRangeType.InvalidRange;
}
public isEmpty(): boolean {
return this.type === NumericRangeType.Empty;
}
public isDefined(): boolean {
return this.type !== NumericRangeType.Empty && this.type !== NumericRangeType.InvalidRange;
}
public toString(): string {
function array_range_to_string(values: number[]): string {
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: any) {
return values
.map((value: any) => {
return Array.isArray(value) ? array_range_to_string(value) : value.toString(10);
})
.join(",");
}
switch (this.type) {
case NumericRangeType.SingleValue:
return (this.value as any).toString(10);
case NumericRangeType.ArrayRange:
return array_range_to_string(this.value as number[]);
case NumericRangeType.Empty:
return NUMERIC_RANGE_EMPTY_STRING;
case NumericRangeType.MatrixRange:
return matrix_range_to_string(this.value);
default:
assert(this.type === NumericRangeType.InvalidRange);
return "NumericRange:<Invalid>";
}
}
public toJSON(): string {
return this.toString();
}
public toEncodeableString(): UAString {
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 {*}
*/
public extract_values<U, T extends ArrayLike<U>>(array: T, dimensions?: number[] | null): ExtractResult<T> {
const self = this as NumericalRange0;
if (!array) {
return {
array,
statusCode: this.type === NumericRangeType.Empty ? StatusCodes.Good : 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: StatusCodes.BadIndexRangeInvalid };
}
}
public set_values_matrix(
sourceToAlter: { matrix: Buffer | []; dimensions: number[] },
newMatrix: Buffer | []
): { matrix: Buffer | []; statusCode: StatusCode } {
const { matrix, dimensions } = sourceToAlter;
const self = this as NumericalRange0;
assert(dimensions, "expecting valid dimensions here");
if (self.type !== NumericRangeType.MatrixRange) {
// istanbul ignore next
return { matrix, statusCode: StatusCodes.BadTypeMismatch };
}
assert(dimensions.length === 2);
const nbRows = dimensions[0];
const nbCols = dimensions[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: 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: 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: StatusCodes.Good
};
}
public set_values(arrayToAlter: Buffer | [], newValues: Buffer | []): { array: Buffer | [] | null; statusCode: StatusCode } {
assert_array_or_buffer(arrayToAlter);
assert_array_or_buffer(newValues);
const self = this as NumericalRange0;
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: StatusCodes.BadIndexRangeNoData };
default:
return { array: null, statusCode: StatusCodes.BadIndexRangeInvalid };
}
if (high_index >= arrayToAlter.length || low_index >= arrayToAlter.length) {
return { array: null, statusCode: StatusCodes.BadIndexRangeNoData };
}
if (this.type !== NumericRangeType.Empty && newValues.length !== high_index - low_index + 1) {
return { array: null, statusCode: StatusCodes.BadIndexRangeInvalid };
}
const insertInPlace = Array.isArray(arrayToAlter)
? insertInPlaceStandardArray
: arrayToAlter instanceof Buffer
? insertInPlaceBuffer
: insertInPlaceTypedArray;
return {
array: insertInPlace(arrayToAlter, low_index, high_index, newValues),
statusCode: StatusCodes.Good
};
}
}
function slice<U, T extends ArrayLike<U>>(arr: T, start: number, end: number): T {
if (start === 0 && end === arr.length) {
return arr;
}
let res;
if ((arr as any).buffer instanceof ArrayBuffer) {
res = (arr as any).subarray(start, end);
} else if (arr instanceof Buffer) {
res = arr.subarray(start, end);
} else {
assert(typeof (arr as any).slice === "function");
assert(arr instanceof Buffer || arr instanceof Array || typeof arr === "string");
res = (arr as any).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;
}
export interface ExtractResult<T> {
array?: T | null;
statusCode: StatusCode;
dimensions?: number[];
}
function extract_empty<U, T extends ArrayLike<U>>(array: T, dimensions: any): ExtractResult<T> {
return {
array: slice(array, 0, array.length),
dimensions,
statusCode: StatusCodes.Good
};
}
function extract_single_value<U, T extends ArrayLike<U>>(array: T, index: number): ExtractResult<T> {
if (index >= array.length) {
if (typeof array === "string") {
return { array: "" as any as T, statusCode: StatusCodes.BadIndexRangeNoData };
}
return { array: null as any as T, statusCode: StatusCodes.BadIndexRangeNoData };
}
return {
array: slice(array, index, index + 1),
statusCode: StatusCodes.Good
};
}
function extract_array_range<U, T extends ArrayLike<U>>(array: T, low_index: number, high_index: number): ExtractResult<T> {
assert(isFinite(low_index) && isFinite(high_index));
assert(low_index >= 0);
assert(low_index <= high_index);
if (low_index >= array.length) {
if (typeof array === "string") {
return { array: "" as any as T, statusCode: StatusCodes.BadIndexRangeNoData };
}
return { array: null as any as T, statusCode: StatusCodes.BadIndexRangeNoData };
}
// clamp high index
high_index = Math.min(high_index, array.length - 1);
return {
array: slice(array, low_index, high_index + 1),
statusCode: StatusCodes.Good
};
}
function isArrayLike(value: any): boolean {
return typeof value.length === "number" || Object.prototype.hasOwnProperty.call(value, "length");
}
function extract_matrix_range<U, T extends ArrayLike<U>>(
array: T,
rowRange: number[],
colRange: number[],
dimension?: number[] | null
): ExtractResult<T> {
assert(Array.isArray(rowRange) && Array.isArray(colRange));
if (array.length === 0) {
return {
array: null,
statusCode: StatusCodes.BadIndexRangeNoData
};
}
if (isArrayLike((array as any)[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! as any)[i];
(result.array as any)[i] = extract_array_range(e, colRange[0], colRange[1]).array;
}
return result;
}
if (!dimension) {
return {
array: null,
statusCode: StatusCodes.BadIndexRangeNoData
};
}
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 as any).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 as any)[srcIndex];
c++;
}
r += 1;
}
return {
array: tmp,
dimensions: [nbRowDest, nbColDest],
statusCode: StatusCodes.Good
};
}
function assert_array_or_buffer(array: any) {
assert(Array.isArray(array) || array.buffer instanceof ArrayBuffer || array instanceof Buffer);
}
function insertInPlaceStandardArray(arrayToAlter: any, low: number, high: number, newValues: any): any {
const args = [low, high - low + 1].concat(newValues);
arrayToAlter.splice(...args);
return arrayToAlter;
}
function insertInPlaceTypedArray(arrayToAlter: any, low: number, high: number, newValues: any): any {
if (low === 0 && high === arrayToAlter.length - 1) {
return new arrayToAlter.constructor(newValues);
}
assert(newValues.length === high - low + 1);
arrayToAlter.subarray(low, high + 1).set(newValues);
return arrayToAlter;
}
function insertInPlaceBuffer(bufferToAlter: Buffer | [], low: number, high: number, newValues: any): Buffer {
// 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;
}
assert(newValues.length === high - low + 1);
for (let i = 0; i < newValues.length; i++) {
bufferToAlter[i + low] = newValues[i];
}
return bufferToAlter;
}
function _overlap(l1: number, h1: number, l2: number, h2: number): boolean {
return Math.max(l1, l2) <= Math.min(h1, h2);
}
export function encodeNumericRange(numericRange: NumericRange, stream: OutputBinaryStream): void {
assert(numericRange instanceof NumericRange);
encodeString(numericRange.toEncodeableString(), stream);
}
export function decodeNumericRange(stream: BinaryStream, _value?: NumericRange): NumericRange {
const str = decodeString(stream)!;
return new NumericRange(str);
}
function coerceNumericRange(value: any | string | NumericRange | null | number[]): NumericRange {
if (value instanceof NumericRange) {
return value;
}
if (value === null || value === undefined) {
return new NumericRange();
}
if (value === NUMERIC_RANGE_EMPTY_STRING) {
return new NumericRange();
}
assert(typeof value === "string" || Array.isArray(value));
return new NumericRange(value);
}