Skip to content

Commit

Permalink
Merge pull request #38 from walkframe/fix/prefilter
Browse files Browse the repository at this point in the history
Fix/prefilter for typescript
  • Loading branch information
righ authored Aug 26, 2024
2 parents 84f670d + 014aa00 commit b104389
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 290 deletions.
62 changes: 62 additions & 0 deletions typescript/src/__tests__/filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Dict, SuggestRowType } from "../types";
import { make, makeAsync, sorters, criteria } from "../index";

const machine = ["iPhone", "Pixel", "XPERIA", "ZenFone", "Galaxy"];
const os = ["iOS", "Android"];
const browser = ["FireFox", "Chrome", "Safari"];

test('exclude impossible combinations', () => {
const factors = {machine, os, browser};
const preFilter = (row: Dict) => {
return !(
(row.machine === 'iPhone' && row.os !== 'iOS') ||
(row.machine !== 'iPhone' && row.os === 'iOS')
);
};
const rows = make(factors, { preFilter });
expect(rows.filter(row => row.machine === 'iPhone' && row.os === 'iOS').length).toBe(browser.length);
expect(rows.filter(row => row.machine === 'iPhone' && row.os !== 'iOS').length).toBe(0);
expect(rows.filter(row => row.machine !== 'iPhone' && row.os === 'iOS').length).toBe(0);

expect(rows.filter(row => row.machine === 'Pixel' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'XPERIA' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'ZenFone' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'Galaxy' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);

expect(rows.filter(row => row.machine === 'iPhone' && row.browser === 'FireFox').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'iPhone' && row.browser === 'Chrome').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'iPhone' && row.browser === 'Safari').length).toBeGreaterThanOrEqual(1);

expect(rows.filter(row => row.machine === 'Pixel' && row.browser === 'FireFox').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'Pixel' && row.browser === 'Chrome').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.machine === 'Pixel' && row.browser === 'Safari').length).toBeGreaterThanOrEqual(1);

expect(rows.filter(row => row.os === 'iOS' && row.browser === 'FireFox').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.os === 'iOS' && row.browser === 'Chrome').length).toBeGreaterThanOrEqual(1);
expect(rows.filter(row => row.os === 'iOS' && row.browser === 'Safari').length).toBeGreaterThanOrEqual(1);
});

test('Limited to iphone and iOS combinations only.', () => {
const factors = {machine, os, browser};
const preFilter = (row: SuggestRowType<typeof factors>) => row.machine === 'iPhone' && row.os === 'iOS';
const rows = make(factors, { preFilter });
expect(rows.length).toBe(browser.length);
expect(rows.filter(row => row.machine === 'iPhone' && row.os === 'iOS').length).toBe(browser.length);
expect(rows.filter(row => row.machine === 'Pixel').length).toBe(0);
expect(rows.filter(row => row.os == 'Android').length).toBe(0);
});


test('Use a constant-false function for preFilter', () => {
const factors = {machine, os, browser};
const preFilter = (row: Dict) => false;
const rows = make(factors, { preFilter });
expect(rows).toEqual([]);
});

test('Use the wrong conditional function for preFilter', () => {
const factors = {machine, os, browser};
const preFilter = (row: Dict) => row.machine === 'WindowsPhone';
const rows = make(factors, { preFilter });
expect(rows).toEqual([]);
});
17 changes: 0 additions & 17 deletions typescript/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,6 @@ test('prefilter excludes specified pairs before', () => {
}
});

test('never matching prefilter throws an exception', () => {
const factors = [
["a", "b", "c"],
["d", "e"],
["f"],
];
const preFilter = (row: Dict) => {
if (row[2] === "f") {
return false;
}
return true;
}
expect(() => {
make(factors, { preFilter })
}).toThrow();
});

test("greedy sorter should make rows less than seed's one with 2", () => {
const factors = [
["a", "b", "c"],
Expand Down
277 changes: 277 additions & 0 deletions typescript/src/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@

import hash from "./sorters/hash";
import {
range,
product,
combinations,
len,
getItems,
getCandidate,
ascOrder,
primeGenerator,
unique,
proxyHandler,
} from "./lib";

import {
IndicesType,
FactorsType,
SerialsType,
Scalar,
Dict,
PairByKey,
ParentsType,
CandidateType,
RowType,
OptionsType,
PairType,
SuggestRowType,
} from "./types";
import { NeverMatch, NotReady } from "./exceptions";

export class Row extends Map<Scalar, number> implements RowType {
// index: number
public consumed: PairByKey = new Map();

constructor(row: CandidateType) {
super();
for (const [k, v] of row) {
this.set(k, v);
}
}
getPairKey(...newPair: number[]) {
const pair = [...this.values(), ...newPair];
return unique(pair);
}
copy(row: Row) {
for (let [k, v] of row.entries()) {
this.set(k, v);
}
}
}

export class Controller<T extends FactorsType> {
public factorLength: number;
public factorIsArray: Boolean;

private serials: SerialsType = new Map();
private parents: ParentsType = new Map();
private indices: IndicesType = new Map();
public incomplete: PairByKey = new Map();

private rejected: Set<Scalar> = new Set();
public row: Row;

constructor(public factors: FactorsType, public options: OptionsType<T>) {
this.serialize(factors);
this.setIncomplete();
this.row = new Row([]);
this.factorLength = len(factors);
this.factorIsArray = factors instanceof Array;

// Delete initial pairs that do not satisfy preFilter
for (const [pairKey, pair] of this.incomplete.entries()) {
const cand = this.getCandidate(pair);
const storable = this.storable(cand);
if (storable == null) {
this.incomplete.delete(pairKey);
}
}
}

private serialize(factors: FactorsType) {
let origin = 0;
const primer = primeGenerator();
getItems(factors).map(([subscript, elements]) => {
const lenElements = len(elements);
const serialList: number[] = [];
range(origin, origin + lenElements).map((index) => {
const serial = primer.next().value;
serialList.push(serial);
this.parents.set(serial, subscript);
this.indices.set(serial, index);
});
this.serials.set(subscript, serialList);
origin += lenElements;
});
};

private setIncomplete() {
const { sorter = hash, seed = "" } = this.options;
const pairs: PairType[] = [];
const allKeys = getItems(this.serials).map(([k, _]) => k);
for (const keys of combinations(allKeys, this.pairwiseCount)) {
const comb = range(0, this.pairwiseCount).map((i) => this.serials.get(keys[i]) as PairType);
for (let pair of product(...comb)) {
pair = pair.sort(ascOrder);
pairs.push(pair);
}
}
for (let pair of sorter(pairs, { seed, indices: this.indices })) {
this.incomplete.set(unique(pair), pair);
}
}

setPair(pair: PairType) {
for (let [key, value] of this.getCandidate(pair)) {
this.row.set(key, value);
}
//this.consume(pair);
for (let p of combinations([...this.row.values()], this.pairwiseCount)) {
this.consume(p);
}
}

consume(pair: PairType) {
const pairKey = unique(pair);
const deleted = this.incomplete.delete(pairKey);
if (deleted) {
this.row.consumed.set(pairKey, pair);
}
}

getCandidate(pair: PairType) {
return getCandidate(pair, this.parents);
}

// Returns a negative value if it is unknown if it can be stored.
storable(candidate: CandidateType) {
let num = 0;
for (let [key, el] of candidate) {
let existing: number | undefined = this.row.get(key);
if (typeof existing === "undefined") {
num++;
} else if (existing != el) {
return null;
}
}
if (!this.options.preFilter) {
return num;
}
const candidates: CandidateType = [...this.row.entries()].concat(candidate);
const nxt = new Row(candidates);
const proxy = this.toProxy(nxt);
try {
const ok = this.options.preFilter(proxy);
if (!ok) {
return null;
}
} catch (e) {
if (e instanceof NotReady) {
return -num;
}
throw e
}
return num;
}

isFilled(row: Row): boolean {
return row.size === this.factorLength;
}

toMap(row: Row): Map<Scalar, number[]> {
const result: Map<Scalar, number[]> = new Map();
for (let [key, serial] of row.entries()) {
const index = this.indices.get(serial) as number;
const first = this.indices.get((this.serials.get(key) as PairType)[0]);
// @ts-ignore TS7015
result.set(key, this.factors[key][index - first]);
}
return result;
}

toProxy(row: Row) {
const obj: Dict = {};
for (let [key, value] of this.toMap(row).entries()) {
obj[key] = value;
}
return new Proxy(obj, proxyHandler) as SuggestRowType<T>;
}

toObject(row: Row) {
const obj: Dict = {};
for (let [key, value] of this.toMap(row).entries()) {
obj[key] = value;
}
return obj as SuggestRowType<T>;
}

reset() {
this.row.consumed.forEach((pair, pairKey) => {
this.incomplete.set(pairKey, pair);
});
this.row = new Row([]);
}

discard() {
this.rejected.add(this.row.getPairKey());
this.row = new Row([]);
}

restore() {
const row = this.row;
this.row = new Row([]);
if (this.factorIsArray) {
const map = this.toMap(row);
return getItems(map)
.sort((a, b) => (a[0] > b[0] ? 1 : -1))
.map(([_, v]) => v);
}
return this.toObject(row);
}

close() {
const trier = new Row([...this.row.entries()]);
const kvs = getItems(this.serials);
for (let [k, vs] of kvs) {
for (let v of vs) {
const pairKey = trier.getPairKey(v);
if (this.rejected.has(pairKey)) {
continue;
}
const cand: CandidateType = [[k, v]];
const storable = this.storable(cand);
if (storable == null) {
this.rejected.add(pairKey);
continue;
}
trier.set(k, v);
break;
}
}
this.row.copy(trier);
if (this.isComplete) {
return true;
}
if (trier.size === 0) {
return false;
}
const pairKey = trier.getPairKey();
if (this.rejected.has(pairKey)) {
throw new NeverMatch();
}
this.rejected.add(pairKey);
this.reset();
return false;
}

get pairwiseCount() {
return this.options.length || 2;
}

get isComplete() {
const filled = this.isFilled(this.row);
if (!filled) {
return false;
}
const proxy = this.toProxy(this.row);
try {
return this.options.preFilter ? this.options.preFilter(proxy) : true;
} catch (e) {
if (e instanceof NotReady) {
return false;
}
throw e;
}
}
}
Loading

0 comments on commit b104389

Please sign in to comment.