-
Notifications
You must be signed in to change notification settings - Fork 102
Added support for FIXED types #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
20bb90d
f83394d
578a745
6e7e6bb
5dd8349
d6a27e3
dd2a7b6
2b5b596
0cc6e7e
eb0bd3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ var LobOptions = common.LobOptions; | |
var NormalizedTypeCode = common.NormalizedTypeCode; | ||
var bignum = util.bignum; | ||
var calendar = util.calendar; | ||
var zeropad = require('../util/zeropad'); | ||
var isValidDay = calendar.isValidDay; | ||
var isValidTime = calendar.isValidTime; | ||
var isZeroDay = calendar.isZeroDay; | ||
|
@@ -39,12 +40,14 @@ var REGEX = { | |
}; | ||
|
||
const maxDecimalMantissaLen = 34; | ||
const maxFixedMantissaLen = 38; | ||
|
||
function Writer(types, useCesu8, spatialTypes) { | ||
this._types = types.map(normalizeType); | ||
function Writer(params, options) { | ||
this._types = params.types.map(normalizeType); | ||
this._fractions = params.fractions; | ||
this.reset(); | ||
this._useCesu8 = (useCesu8 === true); | ||
this._spatialTypes = (spatialTypes === 1 ? 1 : 0); | ||
this._useCesu8 = (options && options.useCesu8 === true); | ||
this._spatialTypes = ((options && options.spatialTypes === 1) ? 1 : 0); | ||
} | ||
|
||
function normalizeType(type) { | ||
|
@@ -66,13 +69,13 @@ Writer.prototype.reset = function reset() { | |
Writer.prototype.setValues = function setValues(values) { | ||
this.reset(); | ||
for (var i = 0; i < values.length; i++) { | ||
this.add(this._types[i], values[i]); | ||
this.add(this._types[i], values[i], this._fractions ? this._fractions[i] : undefined); | ||
} | ||
this._params = true; | ||
}; | ||
|
||
exports.create = function createWriter(params, useCesu8, spatialTypes) { | ||
var writer = new Writer(params.types, useCesu8, spatialTypes); | ||
exports.create = function createWriter(params, options) { | ||
var writer = new Writer(params, options); | ||
writer.setValues(params.values); | ||
return writer; | ||
}; | ||
|
@@ -95,9 +98,11 @@ Object.defineProperties(Writer.prototype, { | |
} | ||
}); | ||
|
||
Writer.prototype.add = function add(type, value) { | ||
Writer.prototype.add = function add(type, value, fraction) { | ||
if (typeof value === 'undefined' || value === null) { | ||
this.pushNull(type); | ||
} else if (type === TypeCode.FIXED8 || type === TypeCode.FIXED12 || type === TypeCode.FIXED16) { | ||
this[type](value, fraction); | ||
} else { | ||
this[type](value); | ||
} | ||
|
@@ -491,9 +496,9 @@ Writer.prototype[TypeCode.DOUBLE] = function writeDouble(value) { | |
Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value) { | ||
var decimal; | ||
if (util.isString(value)) { | ||
decimal = stringToDecimal(value); | ||
decimal = stringToDecimal(value, maxDecimalMantissaLen); | ||
} else if (util.isNumber(value)) { | ||
decimal = stringToDecimal(value.toExponential()); | ||
decimal = stringToDecimal(value.toExponential(), maxDecimalMantissaLen); | ||
} else { | ||
throw createInputError('DECIMAL'); | ||
} | ||
|
@@ -503,6 +508,100 @@ Writer.prototype[TypeCode.DECIMAL] = function writeDecimal(value) { | |
this.push(buffer); | ||
}; | ||
|
||
function fixedToDecimal(value, fraction, typeStr) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think toFixedDecimal would be a better name since we aren't converting a 'fixed' value to a decimal, but rather converting a general value to a decimal with a particular format. |
||
var decimal; | ||
// Convert to decimal object with maximum number of digits 38 and minimum exponent is | ||
// -fraction, so there are at most 'fraction' digits after the decimal | ||
if (util.isString(value)) { | ||
decimal = stringToDecimal(value, maxFixedMantissaLen, -fraction, typeStr); | ||
} else if (util.isNumber(value)) { | ||
decimal = stringToDecimal(value.toExponential(), maxFixedMantissaLen, -fraction, typeStr); | ||
} else { | ||
throw createInputError(typeStr); | ||
} | ||
if (decimal.m.length + decimal.e + fraction > maxFixedMantissaLen) { | ||
throw createInputError(typeStr); // Numeric overflow, greater than maximum precision | ||
} | ||
|
||
if ((-decimal.e) < fraction) { | ||
decimal.m += zeropad.ZEROS[fraction + decimal.e]; | ||
} | ||
return decimal; | ||
} | ||
|
||
function writeFixed16Buffer(decimal, buffer, offset) { | ||
bignum.writeUInt128LE(buffer, decimal.m, offset); | ||
if (decimal.s === -1) { | ||
// Apply two's complement conversion | ||
var extraOne = true; | ||
for (var i = offset; i < offset + 16; i++) { | ||
if (extraOne) { | ||
if (buffer[i] !== 0) { | ||
buffer[i] = 0xff - buffer[i] + 1; | ||
extraOne = false; | ||
} else { | ||
buffer[i] = 0; | ||
} | ||
} else { | ||
buffer[i] = 0xff - buffer[i]; | ||
} | ||
} | ||
} | ||
} | ||
|
||
function checkFixedOverflow(decimal, extBuffer, byteLimit, typeStr) { | ||
if (decimal.s === -1) { | ||
for (var i = byteLimit; i < 16; ++i) { | ||
if (extBuffer[i] != 0xff) { | ||
throw createInputError(typeStr); | ||
} | ||
} | ||
if ((extBuffer[byteLimit - 1] & 0x80) == 0) { | ||
throw createInputError(typeStr); | ||
} | ||
} else { | ||
for (var i = byteLimit; i < 16; ++i) { | ||
if (extBuffer[i] != 0) { | ||
throw createInputError(typeStr); | ||
} | ||
} | ||
if (extBuffer[byteLimit - 1] & 0x80) { | ||
throw createInputError(typeStr); | ||
} | ||
} | ||
} | ||
|
||
Writer.prototype[TypeCode.FIXED8] = function writeFixed8(value, fraction) { | ||
var extBuffer = new Buffer(16); | ||
var decimal = fixedToDecimal(value, fraction, 'FIXED8'); | ||
writeFixed16Buffer(decimal, extBuffer, 0); | ||
// Check that the representation does not exceed 8 bytes | ||
checkFixedOverflow(decimal, extBuffer, 8, 'FIXED8'); | ||
var buffer = new Buffer(9); | ||
buffer[0] = TypeCode.FIXED8; | ||
extBuffer.copy(buffer, 1, 0, 8); | ||
this.push(buffer); | ||
} | ||
|
||
Writer.prototype[TypeCode.FIXED12] = function writeFixed12(value, fraction) { | ||
var extBuffer = new Buffer(16); | ||
var decimal = fixedToDecimal(value, fraction, 'FIXED12'); | ||
writeFixed16Buffer(decimal, extBuffer, 0); | ||
// Check that the representation does not exceed 12 bytes | ||
checkFixedOverflow(decimal, extBuffer, 12, 'FIXED12'); | ||
var buffer = new Buffer(13); | ||
buffer[0] = TypeCode.FIXED12; | ||
extBuffer.copy(buffer, 1, 0, 12); | ||
this.push(buffer); | ||
} | ||
|
||
Writer.prototype[TypeCode.FIXED16] = function writeFixed16(value, fraction) { | ||
var buffer = new Buffer(17); | ||
buffer[0] = TypeCode.FIXED16; | ||
writeFixed16Buffer(fixedToDecimal(value, fraction, 'FIXED16'), buffer, 1); | ||
this.push(buffer); | ||
} | ||
|
||
Writer.prototype[TypeCode.NSTRING] = function writeNString(value) { | ||
this.writeCharacters(TypeCode.NSTRING, value); | ||
}; | ||
|
@@ -531,6 +630,9 @@ Writer.prototype[TypeCode.BLOB] = function writeBLob(value) { | |
var buffer = new Buffer(10); | ||
buffer.fill(0x00); | ||
buffer[0] = TypeCode.BLOB; | ||
if (util.isString(value)) { | ||
value = util.convert.encode(value, this._useCesu8); | ||
} | ||
this.pushLob(buffer, value); | ||
}; | ||
|
||
|
@@ -772,6 +874,35 @@ Writer.prototype[TypeCode.ST_GEOMETRY] = function writeST_GEOMETRY(value) { | |
} | ||
} | ||
|
||
Writer.prototype[TypeCode.BOOLEAN] = function writeBoolean(value) { | ||
var buffer = new Buffer(2); | ||
buffer[0] = TypeCode.BOOLEAN; | ||
// 0x02 - True, 0x01 - Null, 0x00 - False | ||
if (value === null) { | ||
buffer[1] = 0x01; | ||
} else if (util.isString(value)) { | ||
if (value.toUpperCase() === 'TRUE' || value === '1') { | ||
buffer[1] = 0x02; | ||
} else if (value.toUpperCase() === 'FALSE' || value === '0') { | ||
buffer[1] = 0x00; | ||
} else if (value.toUpperCase() === 'UNKNOWN' || value.length === 0) { | ||
buffer[1] = 0x01; | ||
} else { | ||
throw createInputError('BOOLEAN'); | ||
} | ||
} else if (util.isNumber(value)) { | ||
buffer[1] = value == 0 ? 0x00 : 0x02; | ||
} else if (value === true) { | ||
buffer[1] = 0x02; | ||
} else if (value === false) { | ||
buffer[1] = 0x00; | ||
} else { | ||
throw createInputError('BOOLEAN'); | ||
} | ||
|
||
this.push(buffer); | ||
} | ||
|
||
function setChar(str, i, c) { | ||
if(i >= str.length) return str; | ||
return str.substring(0, i) + c + str.substring(i + 1); | ||
|
@@ -793,12 +924,12 @@ function trimTrailingZeroes(str) { | |
return str.substring(0, i + 1); | ||
} | ||
|
||
function stringToDecimal(str) { | ||
function stringToDecimal(str, maxMantissaLen, minExp, typeStr) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my own testing I discovered a subtle behaviour change. Previously, inserting to a DECIMAL column would truncate decimal values beyond the column scale, whereas now with DFV 8 support these values are being rounded. For example, inserting "123.456" into a DECIMAL(10,2) gives "123.45" with DFV 7, but "123.46" with DFV 8. Both the hana-client driver and the server itself use truncation, and I found a reference to this in the server documentation, so we should treat that as the correct behaviour. |
||
/* jshint bitwise:false */ | ||
var dec = str.match(REGEX.DECIMAL); | ||
// REGEX.DECIMAL will match "." and "" despite these being invalid. | ||
if (!dec || str === "." || str === "") { | ||
throw createInputError('DECIMAL'); | ||
throw createInputError(typeStr === undefined ? 'DECIMAL' : typeStr); | ||
} | ||
var sign = dec[1] === '-' ? -1 : 1; | ||
var mInt = dec[2] || ''; | ||
|
@@ -810,14 +941,27 @@ function stringToDecimal(str) { | |
if(mantissa.length === 0) mantissa = "0"; | ||
exp -= mFrac.length | ||
|
||
// round to maxDecimalMantissaLen digits and increment exp appropriately | ||
if(mantissa.length > maxDecimalMantissaLen) { | ||
var followDigit = mantissa[maxDecimalMantissaLen]; | ||
exp += (mantissa.length - maxDecimalMantissaLen) | ||
mantissa = mantissa.substring(0, maxDecimalMantissaLen); | ||
var calcMaxMantissaLength = maxMantissaLen; | ||
if (minExp !== undefined && exp < minExp) { | ||
// Shift the maxMantissaLen such that the exponent is minExp | ||
calcMaxMantissaLength = exp + mantissa.length - minExp; | ||
if (calcMaxMantissaLength < 0) { | ||
// All digits are rounded away | ||
return {s: 1, m: "0", e: 0}; | ||
} else if (calcMaxMantissaLength == 0) { | ||
// Only the first digit matters | ||
return {s: sign, m: mantissa[0] > '4' ? "1" : "0", e: minExp}; | ||
} | ||
} | ||
|
||
// round to calcMaxMantissaLen digits and increment exp appropriately | ||
if(mantissa.length > calcMaxMantissaLength) { | ||
var followDigit = mantissa[calcMaxMantissaLength]; | ||
exp += (mantissa.length - calcMaxMantissaLength); | ||
mantissa = mantissa.substring(0, calcMaxMantissaLength); | ||
if(followDigit > '4') { | ||
// round up | ||
var i = maxDecimalMantissaLen - 1; | ||
var i = calcMaxMantissaLength - 1; | ||
while(i >= 0 && mantissa[i] === '9') { | ||
i -= 1; | ||
} | ||
|
@@ -830,9 +974,9 @@ function stringToDecimal(str) { | |
mantissa = mantissa.substring(0, i + 1); | ||
mantissa = setChar(mantissa, i, String.fromCharCode(mantissa.charCodeAt(i) + 1)); | ||
} | ||
} else if(mantissa[maxDecimalMantissaLen - 1] === '0') { | ||
} else if(mantissa[calcMaxMantissaLength - 1] === '0') { | ||
var trimmed = trimTrailingZeroes(mantissa); | ||
exp += (maxDecimalMantissaLen - trimmed.length); | ||
exp += (calcMaxMantissaLength - trimmed.length); | ||
mantissa = trimmed; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment here describing the structure of 'params'