Skip to content

Commit

Permalink
Add $round operator and support 'place' argument for $trunc. Fixes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
kofrasa committed Mar 6, 2020
1 parent 6f9dc31 commit fe3cb35
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 31 deletions.
85 changes: 78 additions & 7 deletions dist/mingo.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! mingo.js 2.4.0
//! Copyright (c) 2019 Francis Asante
//! Copyright (c) 2020 Francis Asante
//! MIT

(function (global, factory) {
Expand Down Expand Up @@ -160,7 +160,7 @@ function isFunction(v) {
return jsType(v) === T_FUNCTION;
}
function isNil(v) {
return isNull(v) || isUndefined(v);
return v === null || v === undefined;
}
function isNull(v) {
return v === null;
Expand Down Expand Up @@ -830,6 +830,21 @@ var arithmeticOperators = {
},


/**
* Rounds a number to to a whole integer or to a specified decimal place.
* @param {*} obj
* @param {*} expr
*/
$round: function $round(obj, expr) {
var args = computeValue(obj, expr);
var num = args[0];
var place = args[1];
if (isNil(num) || num === NaN || Math.abs(num) === Infinity) return num;
assert(isNumber(num), '$round expression must resolve to a number.');
return truncate(num, place, true);
},


/**
* Calculates the square root of a positive number and returns the result as a double.
*
Expand Down Expand Up @@ -859,20 +874,76 @@ var arithmeticOperators = {


/**
* Truncates a number to its integer.
* Truncates a number to a whole integer or to a specified decimal place.
*
* @param obj
* @param expr
* @returns {number}
*/
$trunc: function $trunc(obj, expr) {
var n = computeValue(obj, expr);
if (isNil(n)) return null;
assert(isNumber(n) || isNaN(n), '$trunc expression must resolve to a number.');
return Math.trunc(n);
var arr = computeValue(obj, expr);
var num = arr[0];
var places = arr[1];
if (isNil(num) || num === NaN || Math.abs(num) === Infinity) return num;
assert(isNumber(num), '$trunc expression must resolve to a number.');
assert(isNil(places) || isNumber(places) && places > -20 && places < 100, "$trunc expression has invalid place");
return truncate(num, places, false);
}
};

/**
* Truncates integer value to number of places. If roundOff is specified round value instead to the number of places
* @param {Number} num
* @param {Number} places
* @param {Boolean} roundOff
*/
function truncate(num, places, roundOff) {
places = places || 0;
var sign = Math.abs(num) === num ? 1 : -1;
num = Math.abs(num);

var result = Math.trunc(num);
var decimals = num - result;

if (places === 0) {
var firstDigit = Math.trunc(10 * decimals);
if (roundOff && result & 1 === 1 && firstDigit >= 5) {
result++;
}
} else if (places > 0) {
var offset = Math.pow(10, places);
var remainder = Math.trunc(decimals * offset);

// last digit before cut off
var lastDigit = Math.trunc(decimals * offset * 10) % 10;

// add one if last digit is greater than 5
if (roundOff && lastDigit > 5) {
remainder += 1;
}

// compute decimal remainder and add to whole number
result += remainder / offset;
} else if (places < 0) {
// handle negative decimal places
var _offset = Math.pow(10, -1 * places);
var excess = result % _offset;
result = Math.max(0, result - excess);

// for negative values the absolute must increase so we round up the last digit if >= 5
if (roundOff && sign === -1) {
while (excess > 10) {
excess -= excess % 10;
}
if (roundOff && result > 0 && excess >= 5) {
result += _offset;
}
}
}

return result * sign;
}

var arrayOperators = {
/**
* Returns the element at the specified array index.
Expand Down
80 changes: 75 additions & 5 deletions lib/operators/expression/arithmetic.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,20 @@ export const arithmeticOperators = {
return Math.pow(args[0], args[1])
},

/**
* Rounds a number to to a whole integer or to a specified decimal place.
* @param {*} obj
* @param {*} expr
*/
$round (obj, expr) {
let args = computeValue(obj, expr)
let num = args[0]
let place = args[1]
if (isNil(num) || num === NaN || Math.abs(num) === Infinity) return num
assert(isNumber(num), '$round expression must resolve to a number.')
return truncate(num, place, true)
},

/**
* Calculates the square root of a positive number and returns the result as a double.
*
Expand Down Expand Up @@ -204,16 +218,72 @@ export const arithmeticOperators = {
},

/**
* Truncates a number to its integer.
* Truncates a number to a whole integer or to a specified decimal place.
*
* @param obj
* @param expr
* @returns {number}
*/
$trunc (obj, expr) {
let n = computeValue(obj, expr)
if (isNil(n)) return null
assert(isNumber(n) || isNaN(n), '$trunc expression must resolve to a number.')
return Math.trunc(n)
let arr = computeValue(obj, expr)
let num = arr[0]
let places = arr[1]
if (isNil(num) || num === NaN || Math.abs(num) === Infinity) return num
assert(isNumber(num), '$trunc expression must resolve to a number.')
assert(isNil(places) || (isNumber(places) && places > -20 && places < 100), "$trunc expression has invalid place")
return truncate(num, places, false)
}
}

/**
* Truncates integer value to number of places. If roundOff is specified round value instead to the number of places
* @param {Number} num
* @param {Number} places
* @param {Boolean} roundOff
*/
function truncate(num, places, roundOff) {
places = places || 0
let sign = Math.abs(num) === num ? 1 : -1
num = Math.abs(num)

let result = Math.trunc(num)
let decimals = num - result

if (places === 0) {
let firstDigit = Math.trunc(10 * decimals)
if (roundOff && result & 1 === 1 && firstDigit >= 5) {
result++
}
} else if (places > 0) {
let offset = Math.pow(10, places)
let remainder = Math.trunc(decimals * offset)

// last digit before cut off
let lastDigit = Math.trunc(decimals * offset * 10) % 10

// add one if last digit is greater than 5
if (roundOff && lastDigit > 5) {
remainder += 1
}

// compute decimal remainder and add to whole number
result += (remainder / offset)
} else if (places < 0) {
// handle negative decimal places
let offset = Math.pow(10, -1*places)
let excess = result % offset
result = Math.max(0, result - excess)

// for negative values the absolute must increase so we round up the last digit if >= 5
if (roundOff && sign === -1) {
while (excess > 10) {
excess -= excess % 10
}
if (result > 0 && excess >= 5) {
result += offset
}
}
}

return result * sign
}
2 changes: 1 addition & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function isObjectLike (v) { return v === Object(v) } // objects, arrays,
export function isDate (v) { return jsType(v) === T_DATE }
export function isRegExp (v) { return jsType(v) === T_REGEXP }
export function isFunction (v) { return jsType(v) === T_FUNCTION }
export function isNil (v) { return isNull(v) || isUndefined(v) }
export function isNil (v) { return v === null || v === undefined }
export function isNull (v) { return v === null }
export function isUndefined (v) { return v === undefined }
export function inArray (arr, item) { return arr.includes(item) }
Expand Down
70 changes: 52 additions & 18 deletions test/expression/arithmetic_operators.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ var runTest = require('./../support').runTest


// hook in custom operator to round value
mingo.addOperators(mingo.OP_EXPRESSION, (_) => {
return {
$round: (obj, expr) => {
var args = _.computeValue(obj, expr)
var n = args[0].toString()
var parts = n.toString().split('.')
return (parts.length > 1)
? Number(parts[0] + '.' + parts[1].substr(0, args[1]))
: n
}
}
})
// mingo.addOperators(mingo.OP_EXPRESSION, (_) => {
// return {
// $round: (obj, expr) => {
// var args = _.computeValue(obj, expr)
// var n = args[0].toString()
// var parts = n.toString().split('.')
// return (parts.length > 1)
// ? Number(parts[0] + '.' + parts[1].substr(0, args[1]))
// : n
// }
// }
// })

runTest("Arithmetic Operators", {
$abs: [
Expand Down Expand Up @@ -92,6 +92,27 @@ runTest("Arithmetic Operators", {
[{ $pow: [ 5, -2 ] }, 0.04],
[{ $pow: [ -5, 0.5 ] }, NaN]
],
$round: [
[[10.5, 0], 10],
[[11.5, 0], 12],
[[12.5, 0], 12],
[[13.5, 0], 14],
// rounded to the first decimal place
[[19.25, 1], 19.2],
[[28.73, 1], 28.7],
[[34.32, 1], 34.3],
[[-45.39, 1], -45.4],
// rounded using the first digit to the left of the decimal
[[19.25, -1], 10],
[[28.73, -1], 20],
[[34.32, -1], 30],
[[-45.39, -1], -50],
// rounded to the whole integer
[[19.25, 0], 19],
[[28.73, 0], 28],
[[34.32, 0], 34],
[[-45.39, 0], -45]
],
$sqrt: [
[{ $sqrt: null }, null],
[{ $sqrt: NaN }, NaN],
Expand All @@ -103,11 +124,24 @@ runTest("Arithmetic Operators", {
[[-1, 2], -3],
[[2, -1], 3]
],
$truc: [
[{ $trunc: NaN }, NaN],
[{ $trunc: null }, null],
[{ $trunc: 0 }, 0],
[{ $trunc: 7.80 }, 7],
[{ $trunc: -2.3 }, -2]
$trunc: [
[[NaN, 0], NaN],
[[null, 0], null],
[[0, 0], 0],
// truncate to the first decimal place
[[19.25, 1], 19.2],
[[28.73, 1], 28.7],
[[34.32, 1], 34.3],
[[-45.39, 1], -45.3],
// truncated to the first place
[[19.25, -1], 10],
[[28.73, -1], 20],
[[34.32, -1], 30],
[[-45.39, -1], -40],
// truncate to the whole integer
[[19.25, 0], 19],
[[28.73, 0], 28],
[[34.32, 0], 34],
[[-45.39, 0], -45]
]
})

0 comments on commit fe3cb35

Please sign in to comment.