/* global NumericRange*/
"use strict";
/**
* @module opcua.datamodel
*/
const assert = require("node-opcua-assert").assert;
const _ = require("underscore");
const StatusCodes = require("node-opcua-status-code").StatusCodes;
const Enum = require("node-opcua-enum");
const ec = require("node-opcua-basic-types");
// 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 NumericRangeEmpty_str = "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'
//
const NumericRange_Schema = {
name: "NumericRange",
subtype: "UAString",
defaultValue: function () {
return new NumericRange();
},
encode: function (value, stream) {
assert(value === null || value instanceof NumericRange);
value = (value === null) ? null : value.toEncodeableString();
ec.encodeString(value, stream);
},
decode: function (stream) {
const str = ec.decodeString(stream);
return new NumericRange(str);
},
coerce: function (value) {
if (value instanceof NumericRange) {
return value;
}
if (value === null || value === undefined) {
return new NumericRange();
}
if (value === NumericRangeEmpty_str) {
return new NumericRange();
}
assert(typeof value === "string" || _.isArray(value));
return new NumericRange(value);
},
random: function () {
function r() {
return Math.ceil(Math.random() * 100);
}
const start = r();
const end = start + r();
return new NumericRange(start, end);
}
};
exports.NumericRange_Schema = NumericRange_Schema;
const factories = require("node-opcua-factory");
factories.registerBasicType(NumericRange_Schema);
const 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(function (a) {
return 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) {
return e.type === NumericRangeType.SingleValue ? [e.value, e.value] : 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) {
let rowRange, colRange;
const elements = values.map(construct_numeric_range_bit_from_string);
rowRange = elements[0];
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(self, value) {
const nr = construct_numeric_range_from_string(value);
self.type = nr.type;
self.value = nr.value;
}
function _construct_from_values(self, value, second_value) {
if (_.isUndefined(second_value)) {
self._set_single_value(value);
} else {
if (!_.isFinite(second_value)) {
throw new Error(" invalid second argument, expecting a number");
}
self._set_range_value(value, second_value);
}
}
function _construct_from_array(self, value) {
assert(value.length === 2);
if (_.isFinite(value[0])) {
if (!_.isFinite(value[1])) {
throw new Error(" invalid range in " + value);
}
self._set_range_value(value[0], value[1]);
}
}
function _construct_from_NumericRange(self, value) {
self.value = _.clone(value);
self.type = value.type;
}
function NumericRange(value, second_value) {
const self = this;
assert(!value || !(value instanceof NumericRange), "use coerce to create a NumericRange");
if (typeof value === "string") {
_construct_from_string(self, value);
} else if (_.isFinite(value) && !_.isUndefined(value)) {
_construct_from_values(self, value, second_value);
} else if (_.isArray(value)) {
_construct_from_array(self, value);
} else if (value instanceof NumericRange) {
_construct_from_NumericRange(self, value);
} else {
this.value = "<invalid>";
this.type = NumericRangeType.Empty;
}
assert((this.type !== NumericRangeType.ArrayRange) || _.isArray(this.value));
}
NumericRange.coerce = NumericRange_Schema.coerce;
NumericRange.prototype._set_single_value = function (value) {
assert(_.isFinite(value));
this.value = value;
this.type = NumericRangeType.SingleValue;
if (this.value < 0) {
this.type = NumericRangeType.InvalidRange;
}
};
NumericRange.prototype._set_range_value = function (low, high) {
assert(_.isFinite(low));
assert(_.isFinite(high));
this.value = [low, high];
this.type = NumericRangeType.ArrayRange;
if (!this._check_range()) {
this.type = NumericRangeType.InvalidRange;
}
};
NumericRange.prototype.isValid = function () {
return this.type !== NumericRange.NumericRangeType.InvalidRange;
};
NumericRange.prototype.isEmpty = function () {
return this.type === NumericRange.NumericRangeType.Empty;
};
NumericRange.prototype._check_range = function () {
if (this.type === NumericRangeType.MatrixRange) {
assert(_.isNumber(this.value[0][0]));
assert(_.isNumber(this.value[0][1]));
assert(_.isNumber(this.value[1][0]));
assert(_.isNumber(this.value[1][1]));
return _valid_range(this.value[0][0], this.value[0][1]) &&
_valid_range(this.value[1][0], this.value[1][1]);
} else if (this.type === NumericRangeType.ArrayRange) {
return _valid_range(this.value[0], this.value[1]);
} else if (this.type === NumericRangeType.SingleValue) {
return this.value >= 0;
}
return true;
};
NumericRange.NumericRangeType = NumericRangeType;
NumericRange.prototype.toEncodeableString = function () {
switch (this.type) {
case NumericRangeType.SingleValue:
case NumericRangeType.ArrayRange:
case NumericRangeType.MatrixRange:
return this.toString();
case NumericRangeType.InvalidRange:
return this.value; // value contains the origianl strings which was detected invalid
default:
return null;
}
};
NumericRange.prototype.toString = function () {
function array_range_to_string(values) {
assert(_.isArray(values));
if (values.length === 2 && values[0] === values[1]) {
return values[0].toString();
}
return values.map(function (value) {
return value.toString(10);
}).join(":");
}
function matrix_range_to_string(values) {
return values.map(function (value) {
return (_.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 NumericRangeEmpty_str;
case NumericRangeType.MatrixRange:
return matrix_range_to_string(this.value);
default:
assert(this.type === NumericRangeType.InvalidRange);
return "NumericRange:<Invalid>";
}
};
NumericRange.prototype.toJSON = function () {
return this.toString();
};
NumericRange.prototype.isDefined = function () {
return this.type !== NumericRangeType.Empty && this.type !== NumericRangeType.InvalidRange;
};
function slice(arr, start, end) {
assert(arr, "expecting value to slice");
if (start===0 && end === arr.length) {
return arr;
}
let res;
//xx console.log("arr",arr.constructor.name,arr.length,start,end);
if (arr.buffer instanceof ArrayBuffer) {
//xx console.log("XXXX ERN ERN ERN 2");
res = arr.subarray(start, end);
} else {
//xx console.log("XXXX ERN ERN ERN 3");
assert(_.isFunction(arr.slice));
assert(arr instanceof Buffer || arr instanceof Array || typeof arr === "string");
res = arr.slice(start, end);
}
if (res instanceof Uint8Array && arr instanceof Buffer) {
//xx console.log("XXXX ERN ERN ERN 1");
// note in iojs 3.00 onward standard Buffer are implemented differently and
// provides a buffer member and a subarray method, in fact in iojs 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: dimensions,
statusCode: StatusCodes.Good
};
}
function extract_single_value(array, index) {
if (index >= array.length) {
return {array: [], statusCode: StatusCodes.BadIndexRangeNoData};
}
return {
array: slice(array, index, index + 1),
statusCode: StatusCodes.Good
};
}
function extract_array_range(array, low_index, high_index) {
assert(_.isFinite(low_index) && _.isFinite(high_index));
assert(low_index >= 0);
assert(low_index <= high_index);
if (low_index >= array.length) {
return {array: [], 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) {
return _.isNumber(value.length) || value.hasOwnProperty("length");
}
function extract_matrix_range(array, rowRange, colRange, dimension) {
assert(_.isArray(rowRange) && _.isArray(colRange));
if (array.length === 0) {
return {
array: [],
statusCode: StatusCodes.BadIndexRangeNoData
};
}
if (isArrayLike(array[0]) && !dimension) {
// like extracting data from a one dimensionnal 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: [],
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;
// constrruct an array of the same type with the appropriate length to
// store the extracted matrix.
const tmp = new array.constructor(nbColDest * nbRowDest);
let row, col, r, 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: StatusCodes.Good
};
}
/**
* @method extract_values
* @param array {Array<Any>} flat array containing values
* @param [dimensions = null ]{Array<Number>} dimension of the matrix if data is a matrix
* @return {*}
*/
NumericRange.prototype.extract_values = function (array, dimensions) {
if (!array) {
return {
array: array,
statusCode: this.type === NumericRangeType.Empty ? StatusCodes.Good : StatusCodes.BadIndexRangeNoData
};
}
let index,low_index,high_index,rowRange,colRange;
switch (this.type) {
case NumericRangeType.Empty:
return extract_empty(array, dimensions);
case NumericRangeType.SingleValue:
index = this.value;
return extract_single_value(array, index);
case NumericRangeType.ArrayRange:
low_index = this.value[0];
high_index = this.value[1];
return extract_array_range(array, low_index, high_index);
case NumericRangeType.MatrixRange:
rowRange = this.value[0];
colRange = this.value[1];
return extract_matrix_range(array, rowRange, colRange, dimensions);
default:
return {array: [], statusCode: StatusCodes.BadIndexRangeInvalid};
}
};
function assert_array_or_buffer(array) {
assert(_.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.apply(arrayToAlter, args);
return arrayToAlter;
}
function insertInPlaceTypedArray(arrayToAlter, low, high, newValues) {
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, low, high, newValues) {
if (low === 0 && high === bufferToAlter.length - 1) {
return Buffer.from(newValues);
}
assert(newValues.length === high - low + 1);
for (let i = 0; i < newValues.length; i++) {
bufferToAlter[i + low] = newValues[i];
}
return bufferToAlter;
}
NumericRange.prototype.set_values = function (arrayToAlter, newValues) {
assert_array_or_buffer(arrayToAlter);
assert_array_or_buffer(newValues);
let low_index, high_index;
switch (this.type) {
case NumericRangeType.Empty:
low_index = 0;
high_index = arrayToAlter.length - 1;
break;
case NumericRangeType.SingleValue:
low_index = this.value;
high_index = this.value;
break;
case NumericRangeType.ArrayRange:
low_index = this.value[0];
high_index = this.value[1];
break;
case NumericRangeType.MatrixRange:
// for the time being MatrixRange is not supported
return {array: arrayToAlter, statusCode: StatusCodes.BadIndexRangeNoData};
default:
return {array: [], statusCode: StatusCodes.BadIndexRangeInvalid};
}
if (high_index >= arrayToAlter.length || low_index >= arrayToAlter.length) {
return {array: [], statusCode: StatusCodes.BadIndexRangeNoData};
}
if ((this.type !== NumericRangeType.Empty) && newValues.length !== (high_index - low_index + 1)) {
return {array: [], statusCode: StatusCodes.BadIndexRangeInvalid};
}
const insertInPlace = (_.isArray(arrayToAlter) ? insertInPlaceStandardArray : (arrayToAlter instanceof Buffer ? insertInPlaceBuffer : insertInPlaceTypedArray));
return {
array: insertInPlace(arrayToAlter, low_index, high_index, newValues),
statusCode: StatusCodes.Good
};
};
function _overlap(l1, h1, l2, h2) {
return Math.max(l1, l2) <= Math.min(h1, h2);
}
const empty = new NumericRange();
NumericRange.overlap = function (nr1, nr2) {
nr1 = nr1 || empty;
nr2 = nr2 || empty;
assert(nr1 instanceof NumericRange);
assert(nr2 instanceof NumericRange);
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);
}
console.log(" NR1 = ", nr1.toEncodeableString());
console.log(" NR2 = ", nr2.toEncodeableString());
assert(false, "not implemented yet "); // TODO
};
exports.NumericRange = NumericRange;