From bd887b57d38f8eca0b755c182ea3e26ac5cac54a Mon Sep 17 00:00:00 2001 From: Daniel Sales Date: Fri, 28 Jun 2024 01:00:39 +0200 Subject: [PATCH] fix: Fixed probability in WeightPicker --- package.json | 2 +- src/SimplePicker/PickProcess.ts | 5 +- src/SimplePicker/Picker.ts | 1 + src/SimplePicker/tests/probability.spec.ts | 18 ++++++ src/WeightPicker/Picker.ts | 53 ++++++++++++--- src/WeightPicker/ThrowDart.ts | 2 +- .../tests/Picker-pick-filters.spec.ts | 34 ++++++++++ src/WeightPicker/tests/probability.spec.ts | 43 +++++++++++++ src/WeightPicker/tests/weight.spec.ts | 64 +++++++++++++++++++ 9 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 src/SimplePicker/tests/probability.spec.ts create mode 100644 src/WeightPicker/tests/probability.spec.ts create mode 100644 src/WeightPicker/tests/weight.spec.ts diff --git a/package.json b/package.json index d4d44d7..665a53c 100644 --- a/package.json +++ b/package.json @@ -69,5 +69,5 @@ "test:watch": "jest --watch" }, "types": "dist/index.d.ts", - "version": "2.1.1" + "version": "2.1.2" } diff --git a/src/SimplePicker/PickProcess.ts b/src/SimplePicker/PickProcess.ts index e07b7ea..2ff9df5 100644 --- a/src/SimplePicker/PickProcess.ts +++ b/src/SimplePicker/PickProcess.ts @@ -3,6 +3,8 @@ import { PickerOptions } from "../PickerOptions"; import { ThrowDartFn } from "../ThrowDartFn"; import { generateRandomInteger } from "../random"; +export type GetMaxDartInteger = (data: T[])=> number; + export type PickProcessProps> = { n: number; data: T[]; @@ -10,6 +12,7 @@ export type PickProcessProps> = { pickerOptions: Required; options: PickOptions; throwDart: TD; + getMaxDartInteger: GetMaxDartInteger; }; export class PickProcess> { @@ -130,7 +133,7 @@ export class PickProcess> { return undefined; const dart = generateRandomInteger( { - max: this.#props.data.length, + max: this.#props.getMaxDartInteger(this.#props.data), randomMode: this.#props.pickerOptions.randomMode, } ); const ret = this.#props.throwDart( { diff --git a/src/SimplePicker/Picker.ts b/src/SimplePicker/Picker.ts index da95390..3257e6c 100644 --- a/src/SimplePicker/Picker.ts +++ b/src/SimplePicker/Picker.ts @@ -56,6 +56,7 @@ export class Picker implements IPicker { options: currentPickOptions, pickerOptions: this.#options, throwDart: throwDart, + getMaxDartInteger: ()=>this.innerData.length, } ); const ret = pickProcess.pick(); diff --git a/src/SimplePicker/tests/probability.spec.ts b/src/SimplePicker/tests/probability.spec.ts new file mode 100644 index 0000000..0ec9f2d --- /dev/null +++ b/src/SimplePicker/tests/probability.spec.ts @@ -0,0 +1,18 @@ +import { Picker } from "../Picker"; + +it("probability should be the same for all", () => { + const picker = new Picker([1, 2, 3, 4]); + const picks = picker.pick(10000); + const numberOf = picks.reduce((acc, x) => { + acc[x] = (acc[x] || 0) + 1; + + return acc; + }, {} as Record); + const entries: [string, number][] = Object.entries(numberOf); + + for (const [_key, value] of entries) + expect(value).toBeGreaterThan(0); + + for (const [_key, value] of entries) + expect(entries[0][1] / value).toBeCloseTo(1, 0.5); +} ); diff --git a/src/WeightPicker/Picker.ts b/src/WeightPicker/Picker.ts index 69f4413..1f04d49 100644 --- a/src/WeightPicker/Picker.ts +++ b/src/WeightPicker/Picker.ts @@ -3,6 +3,7 @@ import { DefaultPickOptions, PickOptions } from "../PickOptions"; import { Picker as IPicker } from "../Picker"; import { PickerOptions } from "../PickerOptions"; import { SimplePicker, SimplePickerPickProcess } from "../SimplePicker"; +import { GetMaxDartInteger } from "../SimplePicker/PickProcess"; import { WeightFixer } from "../WeightFixer"; // eslint-disable-next-line import/no-cycle import { throwDart } from "./ThrowDart"; @@ -10,6 +11,8 @@ import { throwDart } from "./ThrowDart"; export class Picker implements IPicker { #simplePicker: SimplePicker; + #precalcWeight: number | undefined; + /** @internal */ private weightMap: Map; @@ -36,6 +39,9 @@ export class Picker implements IPicker { const removed = this.#simplePicker.filter(...filters); removed.forEach((r) => { + if (this.#precalcWeight !== undefined) + this.#precalcWeight -= this.getWeight(r) as number; + this.weightMap.delete(r); } ); @@ -60,6 +66,16 @@ export class Picker implements IPicker { ...DefaultPickOptions, ...options, }; + const weightWithXElements: Record = {}; + const getMaxDartInteger: GetMaxDartInteger = this.#simplePicker.options.removeOnPick + || currentPickOptions.filters + ? (d)=>{ + if (weightWithXElements[d.length] === undefined) + weightWithXElements[d.length] = calcWeightOfData(d, this.getWeight.bind(this)); + + return weightWithXElements[d.length]; + } + : ()=>this.weight; const pickProcess = new SimplePickerPickProcess( { data: this.#simplePicker.data, n, @@ -72,6 +88,7 @@ export class Picker implements IPicker { getWeight: this.getWeight.bind(this), } ); }, + getMaxDartInteger, } ); const ret = pickProcess.pick(); @@ -89,22 +106,23 @@ export class Picker implements IPicker { // eslint-disable-next-line accessor-pairs get weight(): number { - let size = 0; - - this.#simplePicker.data.forEach((t) => { - const tWeight = this.getWeight(t) as number; - - size += tWeight; - } ); + if (this.#precalcWeight === undefined) + this.#precalcWeight = calcWeightOfData(this.#simplePicker.data, this.getWeight.bind(this)); - return size; + return this.#precalcWeight; } put(item: T, weight: number = 1): Picker { - this.#simplePicker.put(item); - const newWeight = Math.max(0, weight); + if (this.#precalcWeight !== undefined) { + this.#precalcWeight += newWeight; + + this.#precalcWeight -= this.weightMap.get(item) ?? 0; + } + + this.#simplePicker.put(item); + this.weightMap.set(item, newWeight); return this; @@ -123,6 +141,9 @@ export class Picker implements IPicker { const removed = this.#simplePicker.remove(obj); if (removed) { + if (this.#precalcWeight !== undefined) + this.#precalcWeight -= this.weightMap.get(removed) ?? 0; + this.weightMap.delete(removed); return removed; @@ -154,3 +175,15 @@ export class Picker implements IPicker { this.weightMap.clear(); } } + +function calcWeightOfData(data: T[], getWeight: (t: T)=> number | undefined): number { + let size = 0; + + data.forEach((t) => { + const tWeight = getWeight(t) as number; + + size += tWeight; + } ); + + return size; +} diff --git a/src/WeightPicker/ThrowDart.ts b/src/WeightPicker/ThrowDart.ts index b470bad..6d0edac 100644 --- a/src/WeightPicker/ThrowDart.ts +++ b/src/WeightPicker/ThrowDart.ts @@ -10,7 +10,7 @@ type ThisThrowDartParams = ThrowDartParams & { }; const throwDart = ( { data, dart, getWeight }: ThisThrowDartParams): R | undefined => { - if (data.length === 0) + if (data.length === 0 || dart < 0) return undefined; const { accumulated, dartTarget } = getTargetAtDart(dart, data, getWeight); diff --git a/src/WeightPicker/tests/Picker-pick-filters.spec.ts b/src/WeightPicker/tests/Picker-pick-filters.spec.ts index 74b9c33..387520f 100644 --- a/src/WeightPicker/tests/Picker-pick-filters.spec.ts +++ b/src/WeightPicker/tests/Picker-pick-filters.spec.ts @@ -1,3 +1,4 @@ +import { Picker } from "../Picker"; import { pickerStrings1 } from "./fixtures"; it("should keep only odd letters", () => { @@ -45,3 +46,36 @@ it("should keep only odd letters greather than 3", () => { for (const p of picked) expect(p).toMatch(/[bdfhjlnprtvxz]/i); } ); + +it("should pick probability 2:1", () => { + const picker = new Picker([1, 2, 3, 4, 5]); + + for (const d of picker.data) + picker.put(d, d); + + const n = 10000; + const picked = picker.pick(n, { + filters: [ + (r => { + const w = picker.getWeight(r); + + if (w === undefined) + return false; + + return w <= 2; + } ), + ], + } ); + + expect(picker.weight).toBe(15); + + expect(picked.length).toBe(n); + + for (const p of picked) + expect([1, 2].includes(p)).toBeTruthy(); + + const countOf1 = picked.filter(x => x === 1).length; + const countOf2 = picked.filter(x => x === 2).length; + + expect(countOf2 / countOf1).toBeCloseTo(2, 0.5); +} ); diff --git a/src/WeightPicker/tests/probability.spec.ts b/src/WeightPicker/tests/probability.spec.ts new file mode 100644 index 0000000..6e5d038 --- /dev/null +++ b/src/WeightPicker/tests/probability.spec.ts @@ -0,0 +1,43 @@ +import { Picker } from "../Picker"; +import { throwDart } from "../ThrowDart"; + +let picker: Picker; + +beforeEach(() => { + picker = new Picker([1, 2]); + + picker.put(1, 1); + picker.put(2, 2); +} ); + +it("throwDart", () => { + const dartToObj: Record = {}; + + for (let i = -1; i < 4; i++) { + dartToObj[i] = throwDart( { + data: picker.data, + dart: i, + getWeight: picker.getWeight.bind(picker), + } ); + } + + expect(dartToObj[-1]).toBeUndefined(); + expect(dartToObj[0]).toBe(1); + expect(dartToObj[1]).toBe(2); + expect(dartToObj[2]).toBe(2); + expect(dartToObj[3]).toBeUndefined(); +} ); + +it("probability should be 2:1", () => { + const picks = picker.pick(10000); + const numberOf = picks.reduce((acc, x) => { + acc[x] = (acc[x] || 0) + 1; + + return acc; + }, {} as Record); + + for (const [_key, value] of Object.entries(numberOf)) + expect(value).toBeGreaterThan(0); + + expect(numberOf[2] / numberOf[1]).toBeCloseTo(2, 0.5); +} ); diff --git a/src/WeightPicker/tests/weight.spec.ts b/src/WeightPicker/tests/weight.spec.ts new file mode 100644 index 0000000..7cbb8e3 --- /dev/null +++ b/src/WeightPicker/tests/weight.spec.ts @@ -0,0 +1,64 @@ +import { pickerNumbers1, resetData } from "./fixtures"; +beforeEach(() => { + resetData(); +} ); + +it("should get weight of sum of individual weights", () => { + const picker = pickerNumbers1(); + + expect(picker.weight).toBe(21); +} ); + +it("should update total weight without removed elements", () => { + const picker = pickerNumbers1(); + + // eslint-disable-next-line max-len + // Se llama a picker.weight en todos los tests para que internamente se precalcule y se utilice #precalcWeight + expect(picker.weight).toBe(21); + + picker.remove(3); + picker.remove(6); + + expect(picker.weight).toBe(21 - 3 - 6); +} ); + +it("should update total weight with updated individual weight", () => { + const picker = pickerNumbers1(); + + expect(picker.weight).toBe(21); + + picker.put(3, 1); + picker.put(5, 1); + + expect(picker.weight).toBe(21 - 3 + 1 - 5 + 1); +} ); + +it("should update total weight with added individual weight", () => { + const picker = pickerNumbers1(); + + expect(picker.weight).toBe(21); + + picker.put(7, 7); + + expect(picker.weight).toBe(21 + 7); +} ); + +it("should update total weight with weight fixers", () => { + const picker = pickerNumbers1(); + + expect(picker.weight).toBe(21); + + picker.fixWeights((w) => w + 1); + + expect(picker.weight).toBe(21 + 6); +} ); + +it("should only consider non removed items by filters", () => { + const picker = pickerNumbers1(); + + expect(picker.weight).toBe(21); + + picker.filter((r) => r % 2 === 0); + + expect(picker.weight).toBe(2 + 4 + 6); +} );