From be8baf680531d46bfe3b04a741c42295e4ec9eb2 Mon Sep 17 00:00:00 2001 From: abhirupone Date: Fri, 31 Jul 2026 22:05:28 +0530 Subject: [PATCH 1/2] feat: add MODE aggregate function (#2482) --- src/55functions.js | 51 ++++++++++++++++++++++++++++++++++++++++++++ test/test999_mode.js | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 test/test999_mode.js 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}]); + }); +}); From 001da4c930bbc03d511ad3558dbe5539cfe4354b Mon Sep 17 00:00:00 2001 From: abhirupone Date: Tue, 18 Aug 2026 11:38:55 +0530 Subject: [PATCH 2/2] test: rename to test2482.js and add edge case tests for MODE (#2482) --- test/test2482.js | 111 +++++++++++++++++++++++++++++++++++++++++++ test/test999_mode.js | 49 ------------------- 2 files changed, 111 insertions(+), 49 deletions(-) create mode 100644 test/test2482.js delete mode 100644 test/test999_mode.js diff --git a/test/test2482.js b/test/test2482.js new file mode 100644 index 0000000000..44a54efcf0 --- /dev/null +++ b/test/test2482.js @@ -0,0 +1,111 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('Test 2482 - 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 when other values exist', 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. All NULLs or undefined', function () { + var resNull = alasql('SELECT MODE(a) AS m FROM ?', [[{a: null}, {a: null}]]); + assert.deepStrictEqual(resNull, [{m: undefined}]); + + var resUndefined = alasql('SELECT MODE(a) AS m FROM ?', [[{a: undefined}, {a: undefined}]]); + assert.deepStrictEqual(resUndefined, [{m: undefined}]); + + var resMixed = alasql('SELECT MODE(a) AS m FROM ?', [[{a: null}, {a: undefined}]]); + assert.deepStrictEqual(resMixed, [{m: undefined}]); + }); + + it('6. Boolean values', function () { + var resTrue = alasql('SELECT MODE(a) AS m FROM ?', [[{a: true}, {a: false}, {a: true}]]); + assert.deepStrictEqual(resTrue, [{m: true}]); + + var resFalse = alasql('SELECT MODE(a) AS m FROM ?', [[{a: false}, {a: true}, {a: false}]]); + assert.deepStrictEqual(resFalse, [{m: false}]); + + // Tie breaking between booleans: false < true + var resTie = alasql('SELECT MODE(a) AS m FROM ?', [[{a: true}, {a: false}]]); + assert.deepStrictEqual(resTie, [{m: false}]); + }); + + it('7. Strict Equality / Mixed Types', function () { + // 1 and '1' are treated as distinct types + var resNum = alasql('SELECT MODE(a) AS m FROM ?', [[{a: 1}, {a: '1'}, {a: 1}]]); + assert.deepStrictEqual(resNum, [{m: 1}]); + + var resStr = alasql('SELECT MODE(a) AS m FROM ?', [[{a: 1}, {a: '1'}, {a: '1'}]]); + assert.deepStrictEqual(resStr, [{m: '1'}]); + + // 0 vs false + var resZero = alasql('SELECT MODE(a) AS m FROM ?', [[{a: 0}, {a: false}, {a: 0}]]); + assert.deepStrictEqual(resZero, [{m: 0}]); + + // true vs 'true' + var resBoolStr = alasql('SELECT MODE(a) AS m FROM ?', [[{a: true}, {a: 'true'}, {a: true}]]); + assert.deepStrictEqual(resBoolStr, [{m: true}]); + }); + + it('8. Negative Numbers and Floats', function () { + var resNeg = alasql('SELECT MODE(a) AS m FROM ?', [[{a: -10}, {a: -20}, {a: -10}, {a: -30}]]); + assert.deepStrictEqual(resNeg, [{m: -10}]); + + var resFloat = alasql('SELECT MODE(a) AS m FROM ?', [ + [{a: 1.5}, {a: 2.25}, {a: 1.5}, {a: 3.75}], + ]); + assert.deepStrictEqual(resFloat, [{m: 1.5}]); + + var resNegFloat = alasql('SELECT MODE(a) AS m FROM ?', [ + [{a: -3.14}, {a: 2.71}, {a: -3.14}, {a: -0.5}], + ]); + assert.deepStrictEqual(resNegFloat, [{m: -3.14}]); + + // Negative number tie breaking: -10 < -5 + var resNegTie = alasql('SELECT MODE(a) AS m FROM ?', [[{a: -5}, {a: -10}, {a: -5}, {a: -10}]]); + assert.deepStrictEqual(resNegTie, [{m: -10}]); + }); + + it('9. 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}, + {g: 3, v: null}, + {g: 3, v: undefined}, + ]; + 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}, + {g: 3, m: undefined}, + ]); + }); + + it('10. Empty input returns undefined/null', function () { + var res = alasql('SELECT MODE(a) AS m FROM ?', [[]]); + assert.deepStrictEqual(res, [{m: undefined}]); + }); +}); diff --git a/test/test999_mode.js b/test/test999_mode.js deleted file mode 100644 index d3a4eac390..0000000000 --- a/test/test999_mode.js +++ /dev/null @@ -1,49 +0,0 @@ -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}]); - }); -});