Skip to content

Commit

Permalink
fix: Fixed probability in WeightPicker
Browse files Browse the repository at this point in the history
  • Loading branch information
ByDSA committed Jun 27, 2024
1 parent 12d4e1b commit bd887b5
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 13 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@
"test:watch": "jest --watch"
},
"types": "dist/index.d.ts",
"version": "2.1.1"
"version": "2.1.2"
}
5 changes: 4 additions & 1 deletion src/SimplePicker/PickProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { PickerOptions } from "../PickerOptions";
import { ThrowDartFn } from "../ThrowDartFn";
import { generateRandomInteger } from "../random";

export type GetMaxDartInteger<T> = (data: T[])=> number;

export type PickProcessProps<T, TD extends ThrowDartFn<T>> = {
n: number;
data: T[];
onAfterPick?: ((t: T)=> void);
pickerOptions: Required<PickerOptions>;
options: PickOptions<T>;
throwDart: TD;
getMaxDartInteger: GetMaxDartInteger<T>;
};

export class PickProcess<T, TD extends ThrowDartFn<T>> {
Expand Down Expand Up @@ -130,7 +133,7 @@ export class PickProcess<T, TD extends ThrowDartFn<T>> {
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( {
Expand Down
1 change: 1 addition & 0 deletions src/SimplePicker/Picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class Picker<T> implements IPicker<T> {
options: currentPickOptions,
pickerOptions: this.#options,
throwDart: throwDart,
getMaxDartInteger: ()=>this.innerData.length,
} );
const ret = pickProcess.pick();

Expand Down
18 changes: 18 additions & 0 deletions src/SimplePicker/tests/probability.spec.ts
Original file line number Diff line number Diff line change
@@ -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<number, number>);
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);
} );
53 changes: 43 additions & 10 deletions src/WeightPicker/Picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ 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";

export class Picker<T> implements IPicker<T> {
#simplePicker: SimplePicker<T>;

#precalcWeight: number | undefined;

/** @internal */
private weightMap: Map<T, number>;

Expand All @@ -36,6 +39,9 @@ export class Picker<T> implements IPicker<T> {
const removed = this.#simplePicker.filter(...filters);

removed.forEach((r) => {
if (this.#precalcWeight !== undefined)
this.#precalcWeight -= this.getWeight(r) as number;

this.weightMap.delete(r);
} );

Expand All @@ -60,6 +66,16 @@ export class Picker<T> implements IPicker<T> {
...DefaultPickOptions,
...options,
};
const weightWithXElements: Record<number, number> = {};
const getMaxDartInteger: GetMaxDartInteger<T> = 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,
Expand All @@ -72,6 +88,7 @@ export class Picker<T> implements IPicker<T> {
getWeight: this.getWeight.bind(this),
} );
},
getMaxDartInteger,
} );
const ret = pickProcess.pick();

Expand All @@ -89,22 +106,23 @@ export class Picker<T> implements IPicker<T> {

// 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<T> {
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;
Expand All @@ -123,6 +141,9 @@ export class Picker<T> implements IPicker<T> {
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;
Expand Down Expand Up @@ -154,3 +175,15 @@ export class Picker<T> implements IPicker<T> {
this.weightMap.clear();
}
}

function calcWeightOfData<T>(data: T[], getWeight: (t: T)=> number | undefined): number {
let size = 0;

data.forEach((t) => {
const tWeight = getWeight(t) as number;

size += tWeight;
} );

return size;
}
2 changes: 1 addition & 1 deletion src/WeightPicker/ThrowDart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ThisThrowDartParams<T> = ThrowDartParams<T> & {
};

const throwDart = <T, R = T>( { data, dart, getWeight }: ThisThrowDartParams<T>): R | undefined => {
if (data.length === 0)
if (data.length === 0 || dart < 0)
return undefined;

const { accumulated, dartTarget } = getTargetAtDart(dart, data, getWeight);
Expand Down
34 changes: 34 additions & 0 deletions src/WeightPicker/tests/Picker-pick-filters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Picker } from "../Picker";
import { pickerStrings1 } from "./fixtures";

it("should keep only odd letters", () => {
Expand Down Expand Up @@ -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);
} );
43 changes: 43 additions & 0 deletions src/WeightPicker/tests/probability.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Picker } from "../Picker";
import { throwDart } from "../ThrowDart";

let picker: Picker<number>;

beforeEach(() => {
picker = new Picker([1, 2]);

picker.put(1, 1);
picker.put(2, 2);
} );

it("throwDart", () => {
const dartToObj: Record<number, number | undefined> = {};

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<number, number>);

for (const [_key, value] of Object.entries(numberOf))
expect(value).toBeGreaterThan(0);

expect(numberOf[2] / numberOf[1]).toBeCloseTo(2, 0.5);
} );
64 changes: 64 additions & 0 deletions src/WeightPicker/tests/weight.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
} );

0 comments on commit bd887b5

Please sign in to comment.