Skip to content

Commit 9480fbb

Browse files
committed
feat: use a custom result validation filter
1 parent 6960b20 commit 9480fbb

File tree

7 files changed

+191
-75
lines changed

7 files changed

+191
-75
lines changed

packages/keybr-result/lib/result.test.ts

+84-42
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,96 @@
1-
import { test } from "node:test";
1+
import { describe, it, test } from "node:test";
22
import { Layout } from "@keybr/keyboard";
33
import { Histogram } from "@keybr/textinput";
44
import { assert } from "chai";
55
import { Result, speedToTime, timeToSpeed } from "./result.ts";
66
import { TextType } from "./texttype.ts";
77

8-
test("validate", () => {
9-
assert.isFalse(
10-
new Result(
11-
/* layout= */ Layout.EN_US,
12-
/* textType= */ TextType.GENERATED,
13-
/* timeStamp= */ Date.parse("2001-02-03T03:05:06Z"),
14-
/* length= */ 9,
15-
/* time= */ 999,
16-
/* errors= */ 0,
17-
/* histogram= */ new Histogram([]),
18-
).validate(),
8+
describe("validate", () => {
9+
const result = new Result(
10+
/* layout= */ Layout.EN_US,
11+
/* textType= */ TextType.GENERATED,
12+
/* timeStamp= */ Date.parse("2001-02-03T03:05:06Z"),
13+
/* length= */ 10,
14+
/* time= */ 1000,
15+
/* errors= */ 0,
16+
/* histogram= */ new Histogram([
17+
{ codePoint: 0x0061, hitCount: 11, missCount: 1, timeToType: 111 },
18+
{ codePoint: 0x0062, hitCount: 22, missCount: 2, timeToType: 222 },
19+
{ codePoint: 0x0063, hitCount: 33, missCount: 3, timeToType: 333 },
20+
]),
1921
);
2022

21-
assert.isFalse(
22-
new Result(
23-
/* layout= */ Layout.EN_US,
24-
/* textType= */ TextType.GENERATED,
25-
/* timeStamp= */ Date.parse("2001-02-03T03:05:06Z"),
26-
/* length= */ 9,
27-
/* time= */ 999,
28-
/* errors= */ 0,
29-
/* histogram= */ new Histogram([
30-
{ codePoint: 0x0061, hitCount: 11, missCount: 1, timeToType: 111 },
31-
{ codePoint: 0x0062, hitCount: 22, missCount: 2, timeToType: 222 },
32-
{ codePoint: 0x0063, hitCount: 33, missCount: 3, timeToType: 333 },
33-
]),
34-
).validate(),
35-
);
23+
describe("using the default filter", () => {
24+
it("should accept a valid result", () => {
25+
assert.isTrue(result.validate());
26+
assert.isTrue(result.validate({}));
27+
assert.isTrue(result.validate(Result.filter));
28+
});
3629

37-
assert.isTrue(
38-
new Result(
39-
/* layout= */ Layout.EN_US,
40-
/* textType= */ TextType.GENERATED,
41-
/* timeStamp= */ Date.parse("2001-02-03T03:05:06Z"),
42-
/* length= */ 10,
43-
/* time= */ 1000,
44-
/* errors= */ 0,
45-
/* histogram= */ new Histogram([
46-
{ codePoint: 0x0061, hitCount: 11, missCount: 1, timeToType: 111 },
47-
{ codePoint: 0x0062, hitCount: 22, missCount: 2, timeToType: 222 },
48-
{ codePoint: 0x0063, hitCount: 33, missCount: 3, timeToType: 333 },
49-
]),
50-
).validate(),
51-
);
30+
it("should reject if the length is too short", () => {
31+
assert.isFalse(
32+
new Result(
33+
/* layout= */ result.layout,
34+
/* textType= */ result.textType,
35+
/* timeStamp= */ result.timeStamp,
36+
/* length= */ 9,
37+
/* time= */ result.time,
38+
/* errors= */ result.errors,
39+
/* histogram= */ result.histogram,
40+
).validate(),
41+
);
42+
});
43+
44+
it("should reject if the time is too short", () => {
45+
assert.isFalse(
46+
new Result(
47+
/* layout= */ result.layout,
48+
/* textType= */ result.textType,
49+
/* timeStamp= */ result.timeStamp,
50+
/* length= */ result.length,
51+
/* time= */ 999,
52+
/* errors= */ result.errors,
53+
/* histogram= */ result.histogram,
54+
).validate(),
55+
);
56+
});
57+
58+
it("should reject if the complexity is too small", () => {
59+
assert.isFalse(
60+
new Result(
61+
/* layout= */ result.layout,
62+
/* textType= */ result.textType,
63+
/* timeStamp= */ result.timeStamp,
64+
/* length= */ result.length,
65+
/* time= */ result.time,
66+
/* errors= */ result.errors,
67+
/* histogram= */ new Histogram([]),
68+
).validate(),
69+
);
70+
});
71+
});
72+
73+
describe("using a custom filter", () => {
74+
it("should reject if the length is too short", () => {
75+
assert.isFalse(result.validate({ minLength: 100 }));
76+
});
77+
78+
it("should reject if the time is too short", () => {
79+
assert.isFalse(result.validate({ minTime: 60000 }));
80+
});
81+
82+
it("should reject if the complexity is too small", () => {
83+
assert.isFalse(result.validate({ minComplexity: 10 }));
84+
});
85+
86+
it("should reject if the speed is too slow", () => {
87+
assert.isFalse(result.validate({ minSpeed: 1000 }));
88+
});
89+
90+
it("should reject if the speed is too fast", () => {
91+
assert.isFalse(result.validate({ maxSpeed: 1 }));
92+
});
93+
});
5294
});
5395

5496
test("serialize as JSON", () => {

packages/keybr-result/lib/result.ts

+31-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,23 @@ import { type Layout } from "@keybr/keyboard";
22
import { type Histogram, type Stats } from "@keybr/textinput";
33
import { type TextType } from "./texttype.ts";
44

5+
export type Filter = {
6+
readonly minLength: number;
7+
readonly minTime: number;
8+
readonly minComplexity: number;
9+
readonly minSpeed: number;
10+
readonly maxSpeed: number;
11+
};
12+
513
export class Result {
14+
static readonly filter = {
15+
minLength: 10,
16+
minTime: 1000,
17+
minComplexity: 1,
18+
minSpeed: 1,
19+
maxSpeed: Infinity,
20+
} as const satisfies Filter;
21+
622
static fromStats(
723
layout: Layout,
824
textType: TextType,
@@ -51,8 +67,21 @@ export class Result {
5167
this.score = score;
5268
}
5369

54-
validate(): boolean {
55-
return this.length >= 10 && this.time >= 1000 && this.histogram.validate();
70+
validate({
71+
minLength = Result.filter.minLength,
72+
minTime = Result.filter.minTime,
73+
minComplexity = Result.filter.minComplexity,
74+
minSpeed = Result.filter.minSpeed,
75+
maxSpeed = Result.filter.maxSpeed,
76+
}: Partial<Filter> = {}): boolean {
77+
return (
78+
this.length >= minLength &&
79+
this.time >= minTime &&
80+
this.complexity >= minComplexity &&
81+
this.speed >= minSpeed &&
82+
this.speed <= maxSpeed &&
83+
this.histogram.validate()
84+
);
5685
}
5786

5887
toJSON() {

packages/keybr-result/lib/speedunit.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,31 @@ import { defineMessage, type MessageDescriptor } from "react-intl";
44
export class SpeedUnit implements EnumItem {
55
static readonly WPM = new SpeedUnit(
66
"wpm",
7-
(cpm) => cpm / 5,
7+
1 / 5,
88
defineMessage({
99
id: "speedUnit.wpm.name",
1010
defaultMessage: "Words per minute",
1111
}),
1212
);
1313
static readonly WPS = new SpeedUnit(
1414
"wps",
15-
(cpm) => cpm / 300,
15+
1 / 300,
1616
defineMessage({
1717
id: "speedUnit.wps.name",
1818
defaultMessage: "Words per second",
1919
}),
2020
);
2121
static readonly CPM = new SpeedUnit(
2222
"cpm",
23-
(cpm) => cpm,
23+
1,
2424
defineMessage({
2525
id: "speedUnit.cpm.name",
2626
defaultMessage: "Characters per minute",
2727
}),
2828
);
2929
static readonly CPS = new SpeedUnit(
3030
"cps",
31-
(cpm) => cpm / 60,
31+
1 / 60,
3232
defineMessage({
3333
id: "speedUnit.cps.name",
3434
defaultMessage: "Characters per second",
@@ -43,12 +43,16 @@ export class SpeedUnit implements EnumItem {
4343

4444
private constructor(
4545
readonly id: string,
46-
readonly measure: (cpm: number) => number,
46+
readonly factor: number,
4747
readonly name: MessageDescriptor,
4848
) {
4949
Object.freeze(this);
5050
}
5151

52+
measure = (cpm: number): number => {
53+
return cpm * this.factor;
54+
};
55+
5256
toString() {
5357
return this.id;
5458
}

packages/server-cli/lib/command/stats/argument.test.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from "node:test";
22
import { assert } from "chai";
3-
import { parseUserIdRange } from "./argument.ts";
3+
import { parseSpeed, parseUserIdRange } from "./argument.ts";
44

55
test("parse user id range", () => {
66
assert.deepStrictEqual([...parseUserIdRange("1")], [1]);
@@ -20,3 +20,17 @@ test("parse user id range", () => {
2020
parseUserIdRange("2-1");
2121
});
2222
});
23+
24+
test("parse speed", () => {
25+
assert.strictEqual(parseSpeed("100"), 500);
26+
assert.strictEqual(parseSpeed("100wpm"), 500);
27+
assert.strictEqual(parseSpeed("100cpm"), 100);
28+
29+
assert.throws(() => {
30+
parseSpeed("-100");
31+
parseSpeed("+100");
32+
parseSpeed("100.0");
33+
parseSpeed("100xyz");
34+
parseSpeed("100.0xyz");
35+
});
36+
});
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { SpeedUnit } from "@keybr/result";
12
import { InvalidArgumentError } from "commander";
23
import { parseISO, parseJSON } from "date-fns";
34
import { UserIdRange, type UserIdRangeItem } from "./userid-range.ts";
45

5-
export function parseUserIdRange(value: string): UserIdRange {
6+
export function parseUserIdRange(arg: string): UserIdRange {
67
const items: UserIdRangeItem[] = [];
7-
for (const item of value.split(/,/)) {
8+
for (const item of arg.split(/,/)) {
89
const parts = item.split(/-/, 2);
910
if (parts.length === 1) {
1011
const n = parseUserId(parts[0]);
@@ -21,23 +22,33 @@ export function parseUserIdRange(value: string): UserIdRange {
2122
return new UserIdRange(items);
2223
}
2324

24-
function parseUserId(s: string): number {
25-
const n = Number.parseInt(s, 10);
25+
function parseUserId(arg: string): number {
26+
const n = Number.parseInt(arg, 10);
2627
if (!Number.isInteger(n) || n <= 0 || n >= 0xffffffff) {
2728
throw new InvalidArgumentError(`Invalid user id.`);
2829
}
2930
return n;
3031
}
3132

32-
export function parseTimestamp(value: string): Date {
33+
export function parseTimestamp(arg: string): Date {
3334
let date;
34-
date = parseJSON(value);
35-
if (Number.isFinite(date.getTime())) {
35+
if (Number.isFinite((date = parseJSON(arg)).getTime())) {
3636
return date;
3737
}
38-
date = parseISO(value);
39-
if (Number.isFinite(date.getTime())) {
38+
if (Number.isFinite((date = parseISO(arg)).getTime())) {
4039
return date;
4140
}
4241
throw new InvalidArgumentError(`Invalid timestamp.`);
4342
}
43+
44+
export function parseSpeed(arg: string): number {
45+
const m = /^(?<value>[0-9]+)(?<unit>wpm|cpm)?$/.exec(arg);
46+
if (m) {
47+
const { value, unit } = m.groups!;
48+
const { factor } = (
49+
{ wpm: SpeedUnit.WPM, cpm: SpeedUnit.CPM } as Record<string, SpeedUnit>
50+
)[unit || "wpm"];
51+
return Number.parseInt(value, 10) / factor;
52+
}
53+
throw new InvalidArgumentError(`Invalid speed.`);
54+
}

packages/server-cli/lib/command/stats/check-file.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Reader } from "@keybr/binary";
2-
import { type Result } from "@keybr/result";
2+
import { type Filter, type Result } from "@keybr/result";
33
import { readResult, validateHeader } from "@keybr/result-io";
44

55
export type FileStatus =
@@ -16,11 +16,11 @@ export type FileStatus =
1616
readonly results: readonly Result[];
1717
};
1818

19-
export function checkFile(buffer: Uint8Array): FileStatus {
20-
return checkFile0(new Reader(buffer));
21-
}
22-
23-
function checkFile0(reader: Reader): FileStatus {
19+
export function checkFile(
20+
buffer: Uint8Array,
21+
filter: Partial<Filter> = {},
22+
): FileStatus {
23+
const reader = new Reader(buffer);
2424
const results: Result[] = [];
2525
const invalid: Result[] = [];
2626
if (!validateHeader(reader)) {
@@ -33,14 +33,13 @@ function checkFile0(reader: Reader): FileStatus {
3333
} catch {
3434
return { type: "bad", results, invalid };
3535
}
36-
if (result.validate()) {
36+
if (result.validate(filter)) {
3737
results.push(result);
3838
} else {
3939
invalid.push(result);
4040
}
4141
}
42-
if (results.length === 0 || invalid.length > 0) {
43-
return { type: "bad", results, invalid };
44-
}
45-
return { type: "good", results };
42+
return results.length === 0 || invalid.length > 0
43+
? { type: "bad", results, invalid }
44+
: { type: "good", results };
4645
}

0 commit comments

Comments
 (0)