Skip to content

Commit b104389

Browse files
authored
Merge pull request walkframe#38 from walkframe/fix/prefilter
Fix/prefilter for typescript
2 parents 84f670d + 014aa00 commit b104389

File tree

9 files changed

+427
-290
lines changed

9 files changed

+427
-290
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Dict, SuggestRowType } from "../types";
2+
import { make, makeAsync, sorters, criteria } from "../index";
3+
4+
const machine = ["iPhone", "Pixel", "XPERIA", "ZenFone", "Galaxy"];
5+
const os = ["iOS", "Android"];
6+
const browser = ["FireFox", "Chrome", "Safari"];
7+
8+
test('exclude impossible combinations', () => {
9+
const factors = {machine, os, browser};
10+
const preFilter = (row: Dict) => {
11+
return !(
12+
(row.machine === 'iPhone' && row.os !== 'iOS') ||
13+
(row.machine !== 'iPhone' && row.os === 'iOS')
14+
);
15+
};
16+
const rows = make(factors, { preFilter });
17+
expect(rows.filter(row => row.machine === 'iPhone' && row.os === 'iOS').length).toBe(browser.length);
18+
expect(rows.filter(row => row.machine === 'iPhone' && row.os !== 'iOS').length).toBe(0);
19+
expect(rows.filter(row => row.machine !== 'iPhone' && row.os === 'iOS').length).toBe(0);
20+
21+
expect(rows.filter(row => row.machine === 'Pixel' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
22+
expect(rows.filter(row => row.machine === 'XPERIA' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
23+
expect(rows.filter(row => row.machine === 'ZenFone' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
24+
expect(rows.filter(row => row.machine === 'Galaxy' && row.os === 'Android').length).toBeGreaterThanOrEqual(1);
25+
26+
expect(rows.filter(row => row.machine === 'iPhone' && row.browser === 'FireFox').length).toBeGreaterThanOrEqual(1);
27+
expect(rows.filter(row => row.machine === 'iPhone' && row.browser === 'Chrome').length).toBeGreaterThanOrEqual(1);
28+
expect(rows.filter(row => row.machine === 'iPhone' && row.browser === 'Safari').length).toBeGreaterThanOrEqual(1);
29+
30+
expect(rows.filter(row => row.machine === 'Pixel' && row.browser === 'FireFox').length).toBeGreaterThanOrEqual(1);
31+
expect(rows.filter(row => row.machine === 'Pixel' && row.browser === 'Chrome').length).toBeGreaterThanOrEqual(1);
32+
expect(rows.filter(row => row.machine === 'Pixel' && row.browser === 'Safari').length).toBeGreaterThanOrEqual(1);
33+
34+
expect(rows.filter(row => row.os === 'iOS' && row.browser === 'FireFox').length).toBeGreaterThanOrEqual(1);
35+
expect(rows.filter(row => row.os === 'iOS' && row.browser === 'Chrome').length).toBeGreaterThanOrEqual(1);
36+
expect(rows.filter(row => row.os === 'iOS' && row.browser === 'Safari').length).toBeGreaterThanOrEqual(1);
37+
});
38+
39+
test('Limited to iphone and iOS combinations only.', () => {
40+
const factors = {machine, os, browser};
41+
const preFilter = (row: SuggestRowType<typeof factors>) => row.machine === 'iPhone' && row.os === 'iOS';
42+
const rows = make(factors, { preFilter });
43+
expect(rows.length).toBe(browser.length);
44+
expect(rows.filter(row => row.machine === 'iPhone' && row.os === 'iOS').length).toBe(browser.length);
45+
expect(rows.filter(row => row.machine === 'Pixel').length).toBe(0);
46+
expect(rows.filter(row => row.os == 'Android').length).toBe(0);
47+
});
48+
49+
50+
test('Use a constant-false function for preFilter', () => {
51+
const factors = {machine, os, browser};
52+
const preFilter = (row: Dict) => false;
53+
const rows = make(factors, { preFilter });
54+
expect(rows).toEqual([]);
55+
});
56+
57+
test('Use the wrong conditional function for preFilter', () => {
58+
const factors = {machine, os, browser};
59+
const preFilter = (row: Dict) => row.machine === 'WindowsPhone';
60+
const rows = make(factors, { preFilter });
61+
expect(rows).toEqual([]);
62+
});

typescript/src/__tests__/index.test.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,6 @@ test('prefilter excludes specified pairs before', () => {
8989
}
9090
});
9191

92-
test('never matching prefilter throws an exception', () => {
93-
const factors = [
94-
["a", "b", "c"],
95-
["d", "e"],
96-
["f"],
97-
];
98-
const preFilter = (row: Dict) => {
99-
if (row[2] === "f") {
100-
return false;
101-
}
102-
return true;
103-
}
104-
expect(() => {
105-
make(factors, { preFilter })
106-
}).toThrow();
107-
});
108-
10992
test("greedy sorter should make rows less than seed's one with 2", () => {
11093
const factors = [
11194
["a", "b", "c"],

typescript/src/controller.ts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
2+
import hash from "./sorters/hash";
3+
import {
4+
range,
5+
product,
6+
combinations,
7+
len,
8+
getItems,
9+
getCandidate,
10+
ascOrder,
11+
primeGenerator,
12+
unique,
13+
proxyHandler,
14+
} from "./lib";
15+
16+
import {
17+
IndicesType,
18+
FactorsType,
19+
SerialsType,
20+
Scalar,
21+
Dict,
22+
PairByKey,
23+
ParentsType,
24+
CandidateType,
25+
RowType,
26+
OptionsType,
27+
PairType,
28+
SuggestRowType,
29+
} from "./types";
30+
import { NeverMatch, NotReady } from "./exceptions";
31+
32+
export class Row extends Map<Scalar, number> implements RowType {
33+
// index: number
34+
public consumed: PairByKey = new Map();
35+
36+
constructor(row: CandidateType) {
37+
super();
38+
for (const [k, v] of row) {
39+
this.set(k, v);
40+
}
41+
}
42+
getPairKey(...newPair: number[]) {
43+
const pair = [...this.values(), ...newPair];
44+
return unique(pair);
45+
}
46+
copy(row: Row) {
47+
for (let [k, v] of row.entries()) {
48+
this.set(k, v);
49+
}
50+
}
51+
}
52+
53+
export class Controller<T extends FactorsType> {
54+
public factorLength: number;
55+
public factorIsArray: Boolean;
56+
57+
private serials: SerialsType = new Map();
58+
private parents: ParentsType = new Map();
59+
private indices: IndicesType = new Map();
60+
public incomplete: PairByKey = new Map();
61+
62+
private rejected: Set<Scalar> = new Set();
63+
public row: Row;
64+
65+
constructor(public factors: FactorsType, public options: OptionsType<T>) {
66+
this.serialize(factors);
67+
this.setIncomplete();
68+
this.row = new Row([]);
69+
this.factorLength = len(factors);
70+
this.factorIsArray = factors instanceof Array;
71+
72+
// Delete initial pairs that do not satisfy preFilter
73+
for (const [pairKey, pair] of this.incomplete.entries()) {
74+
const cand = this.getCandidate(pair);
75+
const storable = this.storable(cand);
76+
if (storable == null) {
77+
this.incomplete.delete(pairKey);
78+
}
79+
}
80+
}
81+
82+
private serialize(factors: FactorsType) {
83+
let origin = 0;
84+
const primer = primeGenerator();
85+
getItems(factors).map(([subscript, elements]) => {
86+
const lenElements = len(elements);
87+
const serialList: number[] = [];
88+
range(origin, origin + lenElements).map((index) => {
89+
const serial = primer.next().value;
90+
serialList.push(serial);
91+
this.parents.set(serial, subscript);
92+
this.indices.set(serial, index);
93+
});
94+
this.serials.set(subscript, serialList);
95+
origin += lenElements;
96+
});
97+
};
98+
99+
private setIncomplete() {
100+
const { sorter = hash, seed = "" } = this.options;
101+
const pairs: PairType[] = [];
102+
const allKeys = getItems(this.serials).map(([k, _]) => k);
103+
for (const keys of combinations(allKeys, this.pairwiseCount)) {
104+
const comb = range(0, this.pairwiseCount).map((i) => this.serials.get(keys[i]) as PairType);
105+
for (let pair of product(...comb)) {
106+
pair = pair.sort(ascOrder);
107+
pairs.push(pair);
108+
}
109+
}
110+
for (let pair of sorter(pairs, { seed, indices: this.indices })) {
111+
this.incomplete.set(unique(pair), pair);
112+
}
113+
}
114+
115+
setPair(pair: PairType) {
116+
for (let [key, value] of this.getCandidate(pair)) {
117+
this.row.set(key, value);
118+
}
119+
//this.consume(pair);
120+
for (let p of combinations([...this.row.values()], this.pairwiseCount)) {
121+
this.consume(p);
122+
}
123+
}
124+
125+
consume(pair: PairType) {
126+
const pairKey = unique(pair);
127+
const deleted = this.incomplete.delete(pairKey);
128+
if (deleted) {
129+
this.row.consumed.set(pairKey, pair);
130+
}
131+
}
132+
133+
getCandidate(pair: PairType) {
134+
return getCandidate(pair, this.parents);
135+
}
136+
137+
// Returns a negative value if it is unknown if it can be stored.
138+
storable(candidate: CandidateType) {
139+
let num = 0;
140+
for (let [key, el] of candidate) {
141+
let existing: number | undefined = this.row.get(key);
142+
if (typeof existing === "undefined") {
143+
num++;
144+
} else if (existing != el) {
145+
return null;
146+
}
147+
}
148+
if (!this.options.preFilter) {
149+
return num;
150+
}
151+
const candidates: CandidateType = [...this.row.entries()].concat(candidate);
152+
const nxt = new Row(candidates);
153+
const proxy = this.toProxy(nxt);
154+
try {
155+
const ok = this.options.preFilter(proxy);
156+
if (!ok) {
157+
return null;
158+
}
159+
} catch (e) {
160+
if (e instanceof NotReady) {
161+
return -num;
162+
}
163+
throw e
164+
}
165+
return num;
166+
}
167+
168+
isFilled(row: Row): boolean {
169+
return row.size === this.factorLength;
170+
}
171+
172+
toMap(row: Row): Map<Scalar, number[]> {
173+
const result: Map<Scalar, number[]> = new Map();
174+
for (let [key, serial] of row.entries()) {
175+
const index = this.indices.get(serial) as number;
176+
const first = this.indices.get((this.serials.get(key) as PairType)[0]);
177+
// @ts-ignore TS7015
178+
result.set(key, this.factors[key][index - first]);
179+
}
180+
return result;
181+
}
182+
183+
toProxy(row: Row) {
184+
const obj: Dict = {};
185+
for (let [key, value] of this.toMap(row).entries()) {
186+
obj[key] = value;
187+
}
188+
return new Proxy(obj, proxyHandler) as SuggestRowType<T>;
189+
}
190+
191+
toObject(row: Row) {
192+
const obj: Dict = {};
193+
for (let [key, value] of this.toMap(row).entries()) {
194+
obj[key] = value;
195+
}
196+
return obj as SuggestRowType<T>;
197+
}
198+
199+
reset() {
200+
this.row.consumed.forEach((pair, pairKey) => {
201+
this.incomplete.set(pairKey, pair);
202+
});
203+
this.row = new Row([]);
204+
}
205+
206+
discard() {
207+
this.rejected.add(this.row.getPairKey());
208+
this.row = new Row([]);
209+
}
210+
211+
restore() {
212+
const row = this.row;
213+
this.row = new Row([]);
214+
if (this.factorIsArray) {
215+
const map = this.toMap(row);
216+
return getItems(map)
217+
.sort((a, b) => (a[0] > b[0] ? 1 : -1))
218+
.map(([_, v]) => v);
219+
}
220+
return this.toObject(row);
221+
}
222+
223+
close() {
224+
const trier = new Row([...this.row.entries()]);
225+
const kvs = getItems(this.serials);
226+
for (let [k, vs] of kvs) {
227+
for (let v of vs) {
228+
const pairKey = trier.getPairKey(v);
229+
if (this.rejected.has(pairKey)) {
230+
continue;
231+
}
232+
const cand: CandidateType = [[k, v]];
233+
const storable = this.storable(cand);
234+
if (storable == null) {
235+
this.rejected.add(pairKey);
236+
continue;
237+
}
238+
trier.set(k, v);
239+
break;
240+
}
241+
}
242+
this.row.copy(trier);
243+
if (this.isComplete) {
244+
return true;
245+
}
246+
if (trier.size === 0) {
247+
return false;
248+
}
249+
const pairKey = trier.getPairKey();
250+
if (this.rejected.has(pairKey)) {
251+
throw new NeverMatch();
252+
}
253+
this.rejected.add(pairKey);
254+
this.reset();
255+
return false;
256+
}
257+
258+
get pairwiseCount() {
259+
return this.options.length || 2;
260+
}
261+
262+
get isComplete() {
263+
const filled = this.isFilled(this.row);
264+
if (!filled) {
265+
return false;
266+
}
267+
const proxy = this.toProxy(this.row);
268+
try {
269+
return this.options.preFilter ? this.options.preFilter(proxy) : true;
270+
} catch (e) {
271+
if (e instanceof NotReady) {
272+
return false;
273+
}
274+
throw e;
275+
}
276+
}
277+
}

0 commit comments

Comments
 (0)