Skip to content

Commit

Permalink
feat: Implemented $topN and $bottomN accumulators.
Browse files Browse the repository at this point in the history
  • Loading branch information
kofrasa committed Dec 13, 2022
1 parent 7de6aac commit 053db02
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 4 deletions.
5 changes: 4 additions & 1 deletion src/aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { assert, cloneDeep, intersection, isEmpty } from "./util";
* @constructor
*/
export class Aggregator {
constructor(readonly pipeline: Array<RawObject>, readonly options?: Options) {
constructor(
private readonly pipeline: Array<RawObject>,
private readonly options?: Options
) {
this.options = initOptions(options);
}

Expand Down
41 changes: 41 additions & 0 deletions src/operators/accumulator/bottomN.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottomN/#mongodb-group-grp.-bottomN
import { Aggregator } from "../../aggregator";
import { ComputeOptions, computeValue, Options } from "../../core";
import { AnyVal, RawObject } from "../../types";
import { $push } from "./push";

interface InputExpr {
n: number;
sortBy: Record<string, number>;
output: AnyVal;
}

/**
* Returns an aggregation of the bottom n elements within a group, according to the specified sort order.
* If the group contains fewer than n elements, $bottomN returns all elements in the group.
*
* @param {Array} collection The input array
* @param {Object} expr The right-hand side expression value of the operator
* @param {Options} options The options to use for this operation
* @returns {*}
*/
export function $bottomN(
collection: RawObject[],
expr: InputExpr,
options?: Options
): AnyVal[] {
const copts = ComputeOptions.init(options);
const { n, sortBy } = computeValue(
copts.local.groupId,
expr,
null,
copts
) as { n: number; sortBy: Record<string, number> };

const result = new Aggregator([{ $sort: sortBy }], copts.options).run(
collection
);

const m = result.length;
return $push(n >= m ? result : result.slice(m - n), expr.output, copts);
}
2 changes: 2 additions & 0 deletions src/operators/accumulator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export * from "./accumulator";
export * from "./addToSet";
export * from "./avg";
export * from "./bottomN";
export * from "./count";
export * from "./covariancePop";
export * from "./covarianceSamp";
Expand All @@ -17,3 +18,4 @@ export * from "./push";
export * from "./stdDevPop";
export * from "./stdDevSamp";
export * from "./sum";
export * from "./topN";
41 changes: 41 additions & 0 deletions src/operators/accumulator/topN.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/topN/#mongodb-group-grp.-topN
import { Aggregator } from "../../aggregator";
import { ComputeOptions, computeValue, Options } from "../../core";
import { AnyVal, RawObject } from "../../types";
import { $push } from "./push";

interface InputExpr {
n: number;
sortBy: Record<string, number>;
output: AnyVal;
}

/**
* Returns an aggregation of the top n elements within a group, according to the specified sort order.
* If the group contains fewer than n elements, $topN returns all elements in the group.
*
* @param {Array} collection The input array
* @param {Object} expr The right-hand side expression value of the operator
* @param {Options} options The options to use for this operation
* @returns {*}
*/
export function $topN(
collection: RawObject[],
expr: InputExpr,
options?: Options
): AnyVal[] {
const copts = ComputeOptions.init(options);
const { n, sortBy } = computeValue(
copts.local.groupId,
expr,
null,
copts
) as { n: number; sortBy: Record<string, number> };

const result = new Aggregator(
[{ $sort: sortBy }, { $limit: n }],
copts.options
).run(collection);

return $push(result, expr.output, copts);
}
11 changes: 8 additions & 3 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const MAX_LONG = Number.MAX_SAFE_INTEGER;
export const MIN_LONG = Number.MIN_SAFE_INTEGER;

// special value to identify missing items. treated differently from undefined
const MISSING = Object.freeze({});
const MISSING = Symbol("missing");

/**
* Uses the simple hash method as described in Effective Java.
Expand Down Expand Up @@ -129,6 +129,9 @@ export function isEmpty(x: AnyVal): boolean {
(isObject(x) && Object.keys(x).length === 0)
);
}
export function isMissing(m: AnyVal): boolean {
return m === MISSING;
}
// ensure a value is an array or wrapped within one
export function ensureArray(x: AnyVal): RawArray {
return x instanceof Array ? x : [x];
Expand Down Expand Up @@ -173,8 +176,8 @@ export function merge(
options?: MergeOptions
): ArrayOrObject {
// take care of missing inputs
if (target === MISSING) return obj;
if (obj === MISSING) return target;
if (isMissing(target)) return obj;
if (isMissing(obj)) return target;

const inputs = [target, obj];

Expand Down Expand Up @@ -558,6 +561,8 @@ export function hashCode(
* @param {*} b
*/
export function compare(a: AnyVal, b: AnyVal): ComparatorResult {
if (a === MISSING) a = undefined;
if (b === MISSING) b = undefined;
if (a < b) return -1;
if (a > b) return 1;
return 0;
Expand Down
142 changes: 142 additions & 0 deletions test/operators/accumulator/bottomN.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as samples from "../../support";

const docs = [
{ playerId: "PlayerA", gameId: "G1", score: 31 },
{ playerId: "PlayerB", gameId: "G1", score: 33 },
{ playerId: "PlayerC", gameId: "G1", score: 99 },
{ playerId: "PlayerD", gameId: "G1", score: 1 },
{ playerId: "PlayerA", gameId: "G2", score: 10 },
{ playerId: "PlayerB", gameId: "G2", score: 14 },
{ playerId: "PlayerC", gameId: "G2", score: 66 },
{ playerId: "PlayerD", gameId: "G2", score: 80 },
];

samples.runTestPipeline("operators/accumulator/bottomN", [
{
message: "Null and Missing Values",
input: [
{ playerId: "PlayerA", gameId: "G1", score: 1 },
{ playerId: "PlayerB", gameId: "G1", score: 2 },
{ playerId: "PlayerC", gameId: "G1", score: 3 },
{ playerId: "PlayerD", gameId: "G1" },
{ playerId: "PlayerE", gameId: "G1", score: null },
],
pipeline: [
{
$group: {
_id: "$gameId",
playerId: {
$bottomN: {
output: ["$playerId", "$score"],
sortBy: { score: -1 },
n: 3,
},
},
},
},
],
expected: [
{
_id: "G1",
playerId: [
["PlayerA", 1],
["PlayerD", undefined],
["PlayerE", null],
],
},
],
},
{
message: "Data Type Sort Ordering",
input: [
{ playerId: "PlayerA", gameId: "G1", score: 1 },
{ playerId: "PlayerB", gameId: "G1", score: "2" },
{ playerId: "PlayerC", gameId: "G1", score: "" },
],
pipeline: [
{
$group: {
_id: "$gameId",
playerId: {
$bottomN: {
output: ["$playerId", "$score"],
sortBy: { score: -1 },
n: 3,
},
},
},
},
],
expected: [
{
_id: "G1",
playerId: [
["PlayerB", "2"],
["PlayerC", ""],
["PlayerA", 1],
],
},
],
},

{
message: "Finding the Three Lowest Score Documents Across Multiple Games",
input: docs,
pipeline: [
{
$group: {
_id: "$gameId",
playerId: {
$bottomN: {
output: ["$playerId", "$score"],
sortBy: { score: -1 },
n: 3,
},
},
},
},
],
expected: [
{
_id: "G1",
playerId: [
["PlayerB", 33],
["PlayerA", 31],
["PlayerD", 1],
],
},
{
_id: "G2",
playerId: [
["PlayerC", 66],
["PlayerB", 14],
["PlayerA", 10],
],
},
],
},
{
message: "Computing n Based on the Group Key for $group",
input: docs,
pipeline: [
{
$group: {
_id: { gameId: "$gameId" },
gamescores: {
$bottomN: {
output: "$score",
n: {
$cond: { if: { $eq: ["$gameId", "G2"] }, then: 1, else: 3 },
},
sortBy: { score: -1 },
},
},
},
},
],
expected: [
{ _id: { gameId: "G1" }, gamescores: [33, 31, 1] },
{ _id: { gameId: "G2" }, gamescores: [10] },
],
},
]);
Loading

0 comments on commit 053db02

Please sign in to comment.