@bgord/tools 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clock.vo.d.ts +25 -0
- package/dist/clock.vo.js +45 -0
- package/dist/dll.service.d.ts +29 -0
- package/dist/dll.service.js +148 -0
- package/dist/email-mask.service.d.ts +6 -0
- package/dist/email-mask.service.js +14 -0
- package/dist/feature-flag.vo.d.ts +11 -0
- package/dist/feature-flag.vo.js +15 -0
- package/dist/filter.vo.d.ts +17 -0
- package/dist/filter.vo.js +21 -0
- package/dist/hour.vo.d.ts +24 -0
- package/dist/hour.vo.js +52 -0
- package/dist/image.vo.d.ts +5 -0
- package/dist/image.vo.js +3 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/leap-year-checker.service.d.ts +4 -0
- package/dist/leap-year-checker.service.js +9 -0
- package/dist/mean.service.d.ts +4 -0
- package/dist/mean.service.js +11 -0
- package/dist/mime-types.vo.d.ts +2 -0
- package/dist/mime-types.vo.js +7 -0
- package/dist/mime.vo.d.ts +1 -1
- package/dist/min-max-scaler.service.d.ts +36 -0
- package/dist/min-max-scaler.service.js +58 -0
- package/dist/minute.vo.d.ts +14 -0
- package/dist/minute.vo.js +34 -0
- package/dist/money.vo.d.ts +24 -0
- package/dist/money.vo.js +64 -0
- package/dist/outlier-detector.service.d.ts +6 -0
- package/dist/outlier-detector.service.js +13 -0
- package/dist/pagination.service.d.ts +58 -0
- package/dist/pagination.service.js +56 -0
- package/dist/percentage.service.d.ts +4 -0
- package/dist/percentage.service.js +11 -0
- package/dist/population-standard-deviation.service.d.ts +4 -0
- package/dist/population-standard-deviation.service.js +16 -0
- package/dist/random.service.d.ts +8 -0
- package/dist/random.service.js +19 -0
- package/dist/reordering.service.d.ts +61 -0
- package/dist/reordering.service.js +121 -0
- package/dist/revision.vo.d.ts +20 -0
- package/dist/revision.vo.js +45 -0
- package/dist/simple-linear-regression.service.d.ts +18 -0
- package/dist/simple-linear-regression.service.js +50 -0
- package/dist/stepper.service.d.ts +23 -0
- package/dist/stepper.service.js +34 -0
- package/dist/streak-calculator.service.d.ts +14 -0
- package/dist/streak-calculator.service.js +27 -0
- package/dist/sum.service.d.ts +3 -0
- package/dist/sum.service.js +5 -0
- package/dist/thousands-separator.service.d.ts +4 -0
- package/dist/thousands-separator.service.js +6 -0
- package/dist/visually-unambiguous-characters-generator.service.d.ts +4 -0
- package/dist/visually-unambiguous-characters-generator.service.js +35 -0
- package/dist/z-score.service.d.ts +8 -0
- package/dist/z-score.service.js +16 -0
- package/package.json +2 -2
- package/readme.md +1 -0
- package/src/api-key.vo.ts +1 -0
- package/src/clock.vo.ts +67 -0
- package/src/dll.service.ts +185 -0
- package/src/email-mask.service.ts +22 -0
- package/src/feature-flag.vo.ts +19 -0
- package/src/filter.vo.ts +38 -0
- package/src/hour.vo.ts +71 -0
- package/src/image.vo.ts +9 -0
- package/src/index.ts +27 -0
- package/src/leap-year-checker.service.ts +11 -0
- package/src/mean.service.ts +14 -0
- package/src/mime-types.vo.ts +9 -0
- package/src/mime.vo.ts +3 -1
- package/src/min-max-scaler.service.ts +94 -0
- package/src/minute.vo.ts +46 -0
- package/src/money.vo.ts +99 -0
- package/src/outlier-detector.service.ts +20 -0
- package/src/package-version.vo.ts +2 -0
- package/src/pagination.service.ts +111 -0
- package/src/percentage.service.ts +17 -0
- package/src/population-standard-deviation.service.ts +21 -0
- package/src/random.service.ts +29 -0
- package/src/rate-limiter.service.ts +1 -3
- package/src/reordering.service.ts +169 -0
- package/src/revision.vo.ts +61 -0
- package/src/simple-linear-regression.service.ts +74 -0
- package/src/stepper.service.ts +50 -0
- package/src/streak-calculator.service.ts +42 -0
- package/src/sum.service.ts +5 -0
- package/src/thousands-separator.service.ts +7 -0
- package/src/visually-unambiguous-characters-generator.service.ts +42 -0
- package/src/z-score.service.ts +24 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RoundToNearest } from "./rounding.service";
|
|
2
|
+
import { Sum } from "./sum.service";
|
|
3
|
+
export class SimpleLinearRegression {
|
|
4
|
+
constructor(params, rounding) {
|
|
5
|
+
this.params = params;
|
|
6
|
+
this.rounding = new RoundToNearest();
|
|
7
|
+
this.rounding = rounding ?? this.rounding;
|
|
8
|
+
}
|
|
9
|
+
static fromPairs(pairs, rounding) {
|
|
10
|
+
const n = pairs.length;
|
|
11
|
+
if (n < 2) {
|
|
12
|
+
throw new Error("At least two pairs needed");
|
|
13
|
+
}
|
|
14
|
+
const x = pairs.map((pair) => pair.x);
|
|
15
|
+
const y = pairs.map((pair) => pair.y);
|
|
16
|
+
const xx = x.map((x) => x ** 2);
|
|
17
|
+
const xy = pairs.map((pair) => pair.x * pair.y);
|
|
18
|
+
const sX = Sum.of(x);
|
|
19
|
+
if (sX >= Number.MAX_SAFE_INTEGER) {
|
|
20
|
+
throw new Error("Sum of x values is too big");
|
|
21
|
+
}
|
|
22
|
+
const sY = Sum.of(y);
|
|
23
|
+
if (sY >= Number.MAX_SAFE_INTEGER) {
|
|
24
|
+
throw new Error("Sum of y values is too big");
|
|
25
|
+
}
|
|
26
|
+
const sSX = Sum.of(xx);
|
|
27
|
+
if (sSX >= Number.MAX_SAFE_INTEGER) {
|
|
28
|
+
throw new Error("Sum of x squared values is too big");
|
|
29
|
+
}
|
|
30
|
+
const sXY = Sum.of(xy);
|
|
31
|
+
if (sXY >= Number.MAX_SAFE_INTEGER) {
|
|
32
|
+
throw new Error("Sum of x times y values is too big");
|
|
33
|
+
}
|
|
34
|
+
const bDenominator = sSX - sX ** 2 / n;
|
|
35
|
+
if (bDenominator === 0) {
|
|
36
|
+
throw new Error("Unable to create the model");
|
|
37
|
+
}
|
|
38
|
+
const b = (sXY - (sX * sY) / n) / bDenominator;
|
|
39
|
+
const a = (sY - b * sX) / n;
|
|
40
|
+
return new SimpleLinearRegression({ a, b }, rounding);
|
|
41
|
+
}
|
|
42
|
+
predict(x, strategy) {
|
|
43
|
+
const rounding = strategy ?? this.rounding;
|
|
44
|
+
const prediction = this.params.b * x + this.params.a;
|
|
45
|
+
return rounding.round(prediction);
|
|
46
|
+
}
|
|
47
|
+
inspect() {
|
|
48
|
+
return this.params;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type StepType = number;
|
|
2
|
+
type StepperConfigType = {
|
|
3
|
+
total: StepType;
|
|
4
|
+
};
|
|
5
|
+
export declare class Stepper {
|
|
6
|
+
static readonly DEFAULT_CURRENT = 1;
|
|
7
|
+
private current;
|
|
8
|
+
private readonly total;
|
|
9
|
+
constructor(config: StepperConfigType);
|
|
10
|
+
continue(): Stepper;
|
|
11
|
+
read(): {
|
|
12
|
+
finished: boolean;
|
|
13
|
+
raw: {
|
|
14
|
+
current: number;
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
17
|
+
formatted: string;
|
|
18
|
+
};
|
|
19
|
+
reset(): Stepper;
|
|
20
|
+
format(): string;
|
|
21
|
+
isFinished(): boolean;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class Stepper {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.current = Stepper.DEFAULT_CURRENT;
|
|
4
|
+
if (!Number.isInteger(config.total)) {
|
|
5
|
+
throw new Error("Total value is not an integer");
|
|
6
|
+
}
|
|
7
|
+
if (config.total <= Stepper.DEFAULT_CURRENT) {
|
|
8
|
+
throw new Error("Total value should be greater than one");
|
|
9
|
+
}
|
|
10
|
+
this.total = config.total;
|
|
11
|
+
}
|
|
12
|
+
continue() {
|
|
13
|
+
this.current = Math.min(this.current + 1, this.total);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
read() {
|
|
17
|
+
return {
|
|
18
|
+
finished: this.isFinished(),
|
|
19
|
+
raw: { current: this.current, total: this.total },
|
|
20
|
+
formatted: `${this.current}/${this.total}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
reset() {
|
|
24
|
+
this.current = Stepper.DEFAULT_CURRENT;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
format() {
|
|
28
|
+
return this.read().formatted;
|
|
29
|
+
}
|
|
30
|
+
isFinished() {
|
|
31
|
+
return this.current === this.total;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
Stepper.DEFAULT_CURRENT = 1;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { format } from "date-fns";
|
|
2
|
+
type DateType = string;
|
|
3
|
+
export type StreakType = {
|
|
4
|
+
cutoff: DateType;
|
|
5
|
+
dates: DateType[];
|
|
6
|
+
streak: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class StreakCalculator {
|
|
9
|
+
private readonly cutoff;
|
|
10
|
+
constructor();
|
|
11
|
+
calculate(_dates: DateType[]): StreakType;
|
|
12
|
+
static format(date: Parameters<typeof format>[0]): DateType;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { format, isAfter, isEqual, subDays } from "date-fns";
|
|
2
|
+
export class StreakCalculator {
|
|
3
|
+
constructor() {
|
|
4
|
+
const today = new Date();
|
|
5
|
+
this.cutoff = StreakCalculator.format(today);
|
|
6
|
+
}
|
|
7
|
+
calculate(_dates) {
|
|
8
|
+
const dates = Array.from(new Set(_dates));
|
|
9
|
+
let streak = 0;
|
|
10
|
+
let streakTail = this.cutoff;
|
|
11
|
+
for (let i = 0; i < dates.length; i++) {
|
|
12
|
+
const date = dates[i];
|
|
13
|
+
if (isAfter(date, streakTail))
|
|
14
|
+
continue;
|
|
15
|
+
if (isEqual(streakTail, date) || isAfter(date, streakTail)) {
|
|
16
|
+
streakTail = StreakCalculator.format(subDays(date, 1));
|
|
17
|
+
streak++;
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
return { cutoff: this.cutoff, dates, streak };
|
|
23
|
+
}
|
|
24
|
+
static format(date) {
|
|
25
|
+
return format(date, "yyyy-MM-dd");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Random } from "./random.service";
|
|
2
|
+
export class VisuallyUnambiguousCharactersGenerator {
|
|
3
|
+
static generate(length = 1) {
|
|
4
|
+
return Array.from({ length })
|
|
5
|
+
.map(() => VisuallyUnambiguousCharactersGenerator.chars[Random.generate({
|
|
6
|
+
min: 0,
|
|
7
|
+
max: VisuallyUnambiguousCharactersGenerator.chars.length - 1,
|
|
8
|
+
})])
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
VisuallyUnambiguousCharactersGenerator.chars = [
|
|
13
|
+
"a",
|
|
14
|
+
"b",
|
|
15
|
+
"c",
|
|
16
|
+
"d",
|
|
17
|
+
"e",
|
|
18
|
+
"f",
|
|
19
|
+
"h",
|
|
20
|
+
"i",
|
|
21
|
+
"j",
|
|
22
|
+
"k",
|
|
23
|
+
"m",
|
|
24
|
+
"n",
|
|
25
|
+
"o",
|
|
26
|
+
"p",
|
|
27
|
+
"r",
|
|
28
|
+
"s",
|
|
29
|
+
"t",
|
|
30
|
+
"w",
|
|
31
|
+
"x",
|
|
32
|
+
"y",
|
|
33
|
+
"3",
|
|
34
|
+
"4",
|
|
35
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RoundingStrategy } from "./rounding.service";
|
|
2
|
+
export declare class ZScore {
|
|
3
|
+
private readonly rounding;
|
|
4
|
+
private readonly mean;
|
|
5
|
+
private readonly standardDeviation;
|
|
6
|
+
constructor(values: number[], rounding?: RoundingStrategy);
|
|
7
|
+
calculate(value: number): number;
|
|
8
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Mean } from "./mean.service";
|
|
2
|
+
import { PopulationStandardDeviation } from "./population-standard-deviation.service";
|
|
3
|
+
import { RoundToDecimal } from "./rounding.service";
|
|
4
|
+
export class ZScore {
|
|
5
|
+
constructor(values, rounding = new RoundToDecimal(2)) {
|
|
6
|
+
this.rounding = rounding;
|
|
7
|
+
if (values.length < 2) {
|
|
8
|
+
throw new Error("At least two values are needed");
|
|
9
|
+
}
|
|
10
|
+
this.mean = Mean.calculate(values);
|
|
11
|
+
this.standardDeviation = PopulationStandardDeviation.calculate(values);
|
|
12
|
+
}
|
|
13
|
+
calculate(value) {
|
|
14
|
+
return this.rounding.round((value - this.mean) / this.standardDeviation);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bgord/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bartosz Gordon",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"date-fns": "4.1.0",
|
|
33
|
-
"zod": "3.25.
|
|
33
|
+
"zod": "3.25.61"
|
|
34
34
|
},
|
|
35
35
|
"sideEffects": false
|
|
36
36
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @bgord/tools
|
package/src/api-key.vo.ts
CHANGED
package/src/clock.vo.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Hour, HourFormatters } from "./hour.vo";
|
|
2
|
+
import { Minute } from "./minute.vo";
|
|
3
|
+
|
|
4
|
+
export type ClockFormatter = (hour: Hour, minute: Minute) => string;
|
|
5
|
+
|
|
6
|
+
enum ClockFormatterEnum {
|
|
7
|
+
TWENTY_FOUR_HOURS = "TWENTY_FOUR_HOURS",
|
|
8
|
+
TWELVE_HOURS = "TWELVE_HOURS",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ClockFormatters: Record<ClockFormatterEnum, ClockFormatter> = {
|
|
12
|
+
TWENTY_FOUR_HOURS: (hour, minute) => `${hour.get().formatted}:${minute.get().formatted}`,
|
|
13
|
+
|
|
14
|
+
TWELVE_HOURS: (hour, minute) =>
|
|
15
|
+
`${hour.get(HourFormatters.TWELVE_HOURS).formatted}:${minute.get().formatted}`,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export class Clock {
|
|
19
|
+
private readonly formatter: ClockFormatter;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly hour: Hour,
|
|
23
|
+
private readonly minute: Minute,
|
|
24
|
+
formatter?: ClockFormatter,
|
|
25
|
+
) {
|
|
26
|
+
this.formatter = (formatter as ClockFormatter) ?? ClockFormatters.TWENTY_FOUR_HOURS;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(formatter?: ClockFormatter) {
|
|
30
|
+
const format = formatter ?? this.formatter;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
raw: { hour: this.hour.get().raw, minute: this.minute.get().raw },
|
|
34
|
+
formatted: format(this.hour, this.minute),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
equals(another: Clock): boolean {
|
|
39
|
+
return (
|
|
40
|
+
this.hour.get().raw === another.get().raw.hour && this.minute.get().raw === another.get().raw.minute
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isAfter(another: Clock): boolean {
|
|
45
|
+
if (this.hour.get().raw > another.hour.get().raw) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.hour.get().raw === another.hour.get().raw && this.minute.get().raw > another.minute.get().raw) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isBefore(another: Clock): boolean {
|
|
57
|
+
if (this.hour.get().raw < another.hour.get().raw) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.hour.get().raw === another.hour.get().raw && this.minute.get().raw < another.minute.get().raw) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export class Node<T> {
|
|
2
|
+
data: T;
|
|
3
|
+
|
|
4
|
+
prev: Node<T> | null = null;
|
|
5
|
+
|
|
6
|
+
next: Node<T> | null = null;
|
|
7
|
+
|
|
8
|
+
constructor(data: Node<T>["data"]) {
|
|
9
|
+
this.data = data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
forward(n: number): Node<T> | null {
|
|
13
|
+
let currentNode: Node<T> | null = this;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < n; i++) {
|
|
16
|
+
if (currentNode === null) {
|
|
17
|
+
return currentNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
currentNode = currentNode.next;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return currentNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
backward(n: number): Node<T> | null {
|
|
27
|
+
let currentNode: Node<T> | null = this;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
if (currentNode === null) {
|
|
31
|
+
return currentNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
currentNode = currentNode.prev;
|
|
35
|
+
}
|
|
36
|
+
return currentNode;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class DoublyLinkedList<T> {
|
|
41
|
+
static EMPTY_SIZE = 0;
|
|
42
|
+
|
|
43
|
+
private size = DoublyLinkedList.EMPTY_SIZE;
|
|
44
|
+
|
|
45
|
+
private head: Node<T> | null = null;
|
|
46
|
+
|
|
47
|
+
private tail: Node<T> | null = null;
|
|
48
|
+
|
|
49
|
+
getSize(): DoublyLinkedList<T>["size"] {
|
|
50
|
+
return this.size;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
isEmpty(): boolean {
|
|
54
|
+
return this.size === 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getHead(): DoublyLinkedList<T>["head"] {
|
|
58
|
+
return this.head;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getTail(): DoublyLinkedList<T>["tail"] {
|
|
62
|
+
return this.tail;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
append(node: Node<T>): void {
|
|
66
|
+
if (this.head === null || this.tail === null) {
|
|
67
|
+
this.size++;
|
|
68
|
+
|
|
69
|
+
this.head = node;
|
|
70
|
+
this.tail = node;
|
|
71
|
+
} else {
|
|
72
|
+
this.size++;
|
|
73
|
+
|
|
74
|
+
this.tail.next = node;
|
|
75
|
+
node.prev = this.tail;
|
|
76
|
+
|
|
77
|
+
this.tail = node;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
prepend(node: Node<T>): void {
|
|
82
|
+
if (this.head === null || this.tail === null) {
|
|
83
|
+
this.size++;
|
|
84
|
+
|
|
85
|
+
this.head = node;
|
|
86
|
+
this.tail = node;
|
|
87
|
+
} else {
|
|
88
|
+
this.size++;
|
|
89
|
+
|
|
90
|
+
node.next = this.head;
|
|
91
|
+
this.head.prev = node;
|
|
92
|
+
this.head = node;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
clear(): void {
|
|
97
|
+
this.size = 0;
|
|
98
|
+
|
|
99
|
+
this.head = null;
|
|
100
|
+
this.tail = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
remove(node: Node<T>): void {
|
|
104
|
+
if (node.prev) {
|
|
105
|
+
node.prev.next = node.next;
|
|
106
|
+
} else {
|
|
107
|
+
this.head = node.next;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (node.next) {
|
|
111
|
+
node.next.prev = node.prev;
|
|
112
|
+
} else {
|
|
113
|
+
this.tail = node.prev;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.size--;
|
|
117
|
+
node.prev = null;
|
|
118
|
+
node.next = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
insertAfter(node: Node<T>, target: Node<T>): void {
|
|
122
|
+
if (target === this.tail) {
|
|
123
|
+
this.append(node);
|
|
124
|
+
} else {
|
|
125
|
+
this.size++;
|
|
126
|
+
|
|
127
|
+
node.prev = target;
|
|
128
|
+
node.next = target.next;
|
|
129
|
+
// biome-ignore lint: lint/style/noNonNullAssertion
|
|
130
|
+
target.next!.prev = node;
|
|
131
|
+
target.next = node;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
insertBefore(node: Node<T>, target: Node<T>): void {
|
|
136
|
+
if (target === this.head) {
|
|
137
|
+
this.prepend(node);
|
|
138
|
+
} else {
|
|
139
|
+
this.size++;
|
|
140
|
+
|
|
141
|
+
node.next = target;
|
|
142
|
+
node.prev = target.prev;
|
|
143
|
+
// biome-ignore lint: lint/style/noNonNullAssertion
|
|
144
|
+
target.prev!.next = node;
|
|
145
|
+
target.prev = node;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
find(callback: (node: Node<T>) => boolean): Node<T> | null {
|
|
150
|
+
return Array.from(this).find(callback) ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
reverse(): void {
|
|
154
|
+
[this.head, this.tail] = [this.tail, this.head];
|
|
155
|
+
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
for (const node of this) {
|
|
158
|
+
[node.prev, node.next] = [node.next, node.prev];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
toArray(): Node<T>[] {
|
|
163
|
+
return Array.from(this);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static fromArray<T>(array: T[]): DoublyLinkedList<T> {
|
|
167
|
+
const dll = new DoublyLinkedList<T>();
|
|
168
|
+
|
|
169
|
+
for (const item of array) {
|
|
170
|
+
dll.append(new Node<T>(item));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return dll;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
*[Symbol.iterator](): IterableIterator<Node<T>> {
|
|
177
|
+
let current: Node<T> | null = this.head;
|
|
178
|
+
|
|
179
|
+
while (current) {
|
|
180
|
+
yield current;
|
|
181
|
+
|
|
182
|
+
current = current.next;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export const Email = z.email();
|
|
4
|
+
|
|
5
|
+
export type EmailType = z.infer<typeof Email>;
|
|
6
|
+
|
|
7
|
+
export class EmailMask {
|
|
8
|
+
static censor(email: EmailType): EmailType {
|
|
9
|
+
const [beforeAt, afterAt] = email.split("@");
|
|
10
|
+
|
|
11
|
+
const local = beforeAt as string;
|
|
12
|
+
const domain = afterAt as string;
|
|
13
|
+
|
|
14
|
+
if (local.length <= 2) {
|
|
15
|
+
return `${"*".repeat(local.length)}@${domain}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const censoredLocal = `${local.at(0)}${"*".repeat(local.length - 2)}${local.at(-1)}`;
|
|
19
|
+
|
|
20
|
+
return `${censoredLocal}@${domain}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export enum FeatureFlagEnum {
|
|
4
|
+
yes = "yes",
|
|
5
|
+
no = "no",
|
|
6
|
+
}
|
|
7
|
+
export const FeatureFlagValue = z.enum(FeatureFlagEnum);
|
|
8
|
+
|
|
9
|
+
export type FeatureFlagValueType = z.infer<typeof FeatureFlagValue>;
|
|
10
|
+
|
|
11
|
+
export class FeatureFlag {
|
|
12
|
+
static isEnabled(flag: FeatureFlagValueType): boolean {
|
|
13
|
+
return flag === FeatureFlagEnum.yes;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static isDisabled(flag: FeatureFlagValueType): boolean {
|
|
17
|
+
return flag === FeatureFlagEnum.no;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/filter.vo.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export type DefaultFilterSchemaType = z.ZodRawShape;
|
|
4
|
+
|
|
5
|
+
export type FilterValuesType = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export type FilterSchemaType<T extends DefaultFilterSchemaType> = z.ZodObject<T>;
|
|
8
|
+
|
|
9
|
+
export type FilterParseConfigType<T extends DefaultFilterSchemaType> = {
|
|
10
|
+
schema: FilterSchemaType<T>;
|
|
11
|
+
values: FilterValuesType;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class Filter<T extends DefaultFilterSchemaType> {
|
|
15
|
+
constructor(private readonly schema: FilterSchemaType<T>) {}
|
|
16
|
+
|
|
17
|
+
parse(values: FilterValuesType) {
|
|
18
|
+
return Filter._parse({ schema: this.schema, values });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
default() {
|
|
22
|
+
return this.parse({});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static parse<T extends DefaultFilterSchemaType>(config: FilterParseConfigType<T>) {
|
|
26
|
+
return Filter._parse(config);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static default<T extends DefaultFilterSchemaType>(config: Omit<FilterParseConfigType<T>, "values">) {
|
|
30
|
+
return Filter._parse({ schema: config.schema, values: {} });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private static _parse<T extends DefaultFilterSchemaType>(config: FilterParseConfigType<T>) {
|
|
34
|
+
const result = config.schema.safeParse(config.values);
|
|
35
|
+
|
|
36
|
+
return result.success ? result.data : undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/hour.vo.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type HourFormatter = (value: Hour["value"]) => string;
|
|
2
|
+
|
|
3
|
+
export enum HourFormatterEnum {
|
|
4
|
+
TWENTY_FOUR_HOURS = "TWENTY_FOUR_HOURS",
|
|
5
|
+
TWENTY_FOUR_HOURS_WO_PADDING = "TWENTY_FOUR_HOURS_WO_PADDING",
|
|
6
|
+
AM_PM = "AM_PM",
|
|
7
|
+
TWELVE_HOURS = "TWELVE_HOURS",
|
|
8
|
+
TWELVE_HOURS_WO_PADDING = "TWELVE_HOURS_WO_PADDING",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const HourFormatters: Record<HourFormatterEnum, HourFormatter> = {
|
|
12
|
+
TWENTY_FOUR_HOURS: (value) => value.toString().padStart(2, "0"),
|
|
13
|
+
|
|
14
|
+
TWENTY_FOUR_HOURS_WO_PADDING: (value) => value.toString(),
|
|
15
|
+
|
|
16
|
+
AM_PM: (value) => {
|
|
17
|
+
if (value < 12) return `${value.toString()} a.m.`;
|
|
18
|
+
return `${value.toString()} p.m.`;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
TWELVE_HOURS: (value) => (value % 12).toString().padStart(2, "0"),
|
|
22
|
+
|
|
23
|
+
TWELVE_HOURS_WO_PADDING: (value) => (value % 12).toString(),
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export class Hour {
|
|
27
|
+
private readonly value: number;
|
|
28
|
+
|
|
29
|
+
private readonly formatter: HourFormatter;
|
|
30
|
+
|
|
31
|
+
static readonly ZERO = new Hour(0);
|
|
32
|
+
|
|
33
|
+
static readonly MAX = new Hour(23);
|
|
34
|
+
|
|
35
|
+
constructor(candidate: number, formatter?: HourFormatter) {
|
|
36
|
+
if (!Number.isInteger(candidate)) {
|
|
37
|
+
throw new Error("Invalid hour");
|
|
38
|
+
}
|
|
39
|
+
if (candidate < 0) {
|
|
40
|
+
throw new Error("Invalid hour");
|
|
41
|
+
}
|
|
42
|
+
if (candidate >= 24) {
|
|
43
|
+
throw new Error("Invalid hour");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.value = candidate;
|
|
47
|
+
this.formatter = (formatter as HourFormatter) ?? HourFormatters.TWENTY_FOUR_HOURS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get(formatter?: HourFormatter) {
|
|
51
|
+
const format = formatter ?? this.formatter;
|
|
52
|
+
|
|
53
|
+
return { raw: this.value, formatted: format(this.value) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
equals(another: Hour): boolean {
|
|
57
|
+
return this.value === another.get().raw;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isAfter(another: Hour): boolean {
|
|
61
|
+
return this.value > another.get().raw;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isBefore(another: Hour): boolean {
|
|
65
|
+
return this.value < another.get().raw;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static list(formatter?: HourFormatter) {
|
|
69
|
+
return Array.from({ length: 24 }).map((_, index) => new Hour(index, formatter));
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/image.vo.ts
ADDED