diff --git a/src/55functions.js b/src/55functions.js index 9bdc42ff24..9129ef6f52 100644 --- a/src/55functions.js +++ b/src/55functions.js @@ -389,6 +389,57 @@ alasql.aggr.median = alasql.aggr.MEDIAN = function (v, s, stage) { } }; +alasql.aggr.mode = alasql.aggr.MODE = function (v, s, stage) { + if (stage === 2) { + if (v !== undefined && v !== null) { + s.push(v); + } + return s; + } + + if (stage === 1) { + if (v === undefined || v === null) { + return []; + } + return [v]; + } + + if (!s.length) { + return undefined; + } + + let counts = new Map(); + let maxCount = 0; + + for (let i = 0; i < s.length; i++) { + let val = s[i]; + let count = (counts.get(val) || 0) + 1; + counts.set(val, count); + if (count > maxCount) { + maxCount = count; + } + } + + let candidates = []; + for (let [val, count] of counts.entries()) { + if (count === maxCount) { + candidates.push(val); + } + } + + if (candidates.length === 1) { + return candidates[0]; + } + + candidates.sort((a, b) => { + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + + return candidates[0]; +}; + alasql.aggr.QUART = function (v, s, stage, nth) { //Quartile (first quartile per default or input param) if (stage === 2) { diff --git a/test/test999_mode.js b/test/test999_mode.js new file mode 100644 index 0000000000..d3a4eac390 --- /dev/null +++ b/test/test999_mode.js @@ -0,0 +1,49 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('Test MODE aggregate function', function () { + it('1. Basic numbers mode', function () { + var res = alasql('SELECT MODE(a) AS m FROM ?', [[{a: 1}, {a: 2}, {a: 2}, {a: 3}]]); + assert.deepStrictEqual(res, [{m: 2}]); + }); + + it('2. Mode with tie breaking (smallest value)', function () { + var res = alasql('SELECT MODE(a) AS m FROM ?', [[{a: 3}, {a: 1}, {a: 3}, {a: 1}]]); + assert.deepStrictEqual(res, [{m: 1}]); + }); + + it('3. Mode with strings', function () { + var res = alasql('SELECT MODE(a) AS m FROM ?', [ + [{a: 'apple'}, {a: 'banana'}, {a: 'apple'}, {a: 'orange'}], + ]); + assert.deepStrictEqual(res, [{m: 'apple'}]); + }); + + it('4. Mode ignores NULL and undefined values', function () { + var res = alasql('SELECT MODE(a) AS m FROM ?', [[{a: null}, {a: 5}, {a: undefined}, {a: 5}]]); + assert.deepStrictEqual(res, [{m: 5}]); + }); + + it('5. Mode with GROUP BY', function () { + var data = [ + {g: 1, v: 10}, + {g: 1, v: 20}, + {g: 1, v: 20}, + {g: 2, v: 30}, + {g: 2, v: 30}, + {g: 2, v: 40}, + ]; + var res = alasql('SELECT g, MODE(v) AS m FROM ? GROUP BY g ORDER BY g', [data]); + assert.deepStrictEqual(res, [ + {g: 1, m: 20}, + {g: 2, m: 30}, + ]); + }); + + it('6. Empty input returns undefined/null', function () { + var res = alasql('SELECT MODE(a) AS m FROM ?', [[]]); + assert.deepStrictEqual(res, [{m: undefined}]); + }); +});