@bgord/tools 0.15.2 → 0.16.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/basename.vo.d.ts +11 -2
- package/dist/basename.vo.js +22 -13
- package/dist/date-calculator.service.d.ts +2 -2
- package/dist/date-calculator.service.js +10 -11
- package/dist/date-range.vo.d.ts +1 -0
- package/dist/date-range.vo.js +2 -1
- package/dist/day-iso-id.vo.d.ts +3 -0
- package/dist/day-iso-id.vo.js +4 -4
- package/dist/day.vo.js +12 -10
- package/dist/directory-path-absolute.vo.d.ts +5 -0
- package/dist/directory-path-absolute.vo.js +11 -5
- package/dist/directory-path-relative.vo.d.ts +6 -0
- package/dist/directory-path-relative.vo.js +12 -6
- package/dist/dll.service.js +37 -31
- package/dist/extension.vo.d.ts +6 -2
- package/dist/extension.vo.js +11 -9
- package/dist/file-path-absolute-schema.vo.d.ts +5 -0
- package/dist/file-path-absolute-schema.vo.js +12 -6
- package/dist/file-path-relative-schema.vo.d.ts +6 -1
- package/dist/file-path-relative-schema.vo.js +10 -6
- package/dist/file-path.vo.js +4 -4
- package/dist/filename-from-string.vo.d.ts +6 -4
- package/dist/filename-from-string.vo.js +15 -14
- package/dist/filename-suffix.vo.d.ts +4 -2
- package/dist/filename-suffix.vo.js +5 -3
- package/dist/filename.vo.d.ts +2 -2
- package/dist/filename.vo.js +9 -9
- package/dist/height.vo.d.ts +6 -4
- package/dist/height.vo.js +56 -51
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mime.vo.d.ts +3 -1
- package/dist/mime.vo.js +8 -6
- package/dist/month-iso-id.vo.d.ts +3 -0
- package/dist/month-iso-id.vo.js +7 -12
- package/dist/month.vo.js +15 -13
- package/dist/object-key.vo.d.ts +5 -0
- package/dist/object-key.vo.js +16 -6
- package/dist/package-version.vo.d.ts +3 -0
- package/dist/package-version.vo.js +12 -34
- package/dist/pagination.service.d.ts +1 -1
- package/dist/pagination.service.js +11 -11
- package/dist/quarter-iso-id.vo.d.ts +3 -0
- package/dist/quarter-iso-id.vo.js +8 -7
- package/dist/rate-limiter.service.d.ts +3 -2
- package/dist/rate-limiter.service.js +4 -2
- package/dist/reordering.service.d.ts +20 -2
- package/dist/reordering.service.js +49 -29
- package/dist/revision.vo.d.ts +8 -3
- package/dist/revision.vo.js +13 -6
- package/dist/rounding.adapter.js +1 -2
- package/dist/size.vo.d.ts +1 -0
- package/dist/size.vo.js +4 -7
- package/dist/streak-calculator.service.d.ts +3 -4
- package/dist/streak-calculator.service.js +11 -17
- package/dist/time-zone-offset-value.vo.d.ts +1 -1
- package/dist/time-zone-offset-value.vo.js +1 -7
- package/dist/time.service.d.ts +11 -6
- package/dist/time.service.js +31 -18
- package/dist/timezone.vo.js +1 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/week-iso-id.vo.d.ts +3 -0
- package/dist/week-iso-id.vo.js +4 -4
- package/dist/week.vo.js +1 -1
- package/dist/weekday.vo.d.ts +7 -6
- package/dist/weekday.vo.js +20 -13
- package/dist/weight.vo.d.ts +12 -0
- package/dist/weight.vo.js +37 -27
- package/dist/year-iso-id.vo.d.ts +3 -0
- package/dist/year-iso-id.vo.js +4 -6
- package/dist/year.vo.d.ts +2 -0
- package/dist/year.vo.js +4 -2
- package/package.json +1 -1
- package/readme.md +0 -1
- package/src/basename.vo.ts +25 -14
- package/src/clock.vo.ts +1 -0
- package/src/date-calculator.service.ts +10 -15
- package/src/date-range.vo.ts +3 -1
- package/src/day-iso-id.vo.ts +9 -10
- package/src/day.vo.ts +17 -10
- package/src/directory-path-absolute.vo.ts +12 -5
- package/src/directory-path-relative.vo.ts +13 -6
- package/src/dll.service.ts +45 -43
- package/src/extension.vo.ts +14 -12
- package/src/file-path-absolute-schema.vo.ts +15 -6
- package/src/file-path-relative-schema.vo.ts +13 -6
- package/src/file-path.vo.ts +15 -11
- package/src/filename-from-string.vo.ts +20 -15
- package/src/filename-suffix.vo.ts +8 -4
- package/src/filename.vo.ts +14 -15
- package/src/height.vo.ts +71 -53
- package/src/index.ts +0 -1
- package/src/mime.vo.ts +10 -7
- package/src/month-iso-id.vo.ts +10 -20
- package/src/month.vo.ts +19 -13
- package/src/object-key.vo.ts +21 -7
- package/src/outlier-detector.service.ts +1 -0
- package/src/package-version.vo.ts +18 -47
- package/src/pagination.service.ts +15 -13
- package/src/quarter-iso-id.vo.ts +11 -13
- package/src/quarter.vo.ts +3 -0
- package/src/rate-limiter.service.ts +7 -7
- package/src/reordering.service.ts +52 -38
- package/src/revision.vo.ts +17 -8
- package/src/rounding.adapter.ts +1 -3
- package/src/size.vo.ts +6 -16
- package/src/streak-calculator.service.ts +12 -17
- package/src/time-zone-offset-value.vo.ts +2 -7
- package/src/time.service.ts +43 -45
- package/src/timezone.vo.ts +1 -3
- package/src/week-iso-id.vo.ts +13 -14
- package/src/week.vo.ts +4 -2
- package/src/weekday.vo.ts +27 -13
- package/src/weight.vo.ts +49 -30
- package/src/year-iso-id.vo.ts +6 -9
- package/src/year.vo.ts +12 -2
- package/dist/stepper.service.d.ts +0 -23
- package/dist/stepper.service.js +0 -33
- package/src/stepper.service.ts +0 -43
|
@@ -4,49 +4,22 @@ type MajorType = number;
|
|
|
4
4
|
type MinorType = number;
|
|
5
5
|
type PatchType = number;
|
|
6
6
|
|
|
7
|
-
export const
|
|
8
|
-
.string()
|
|
9
|
-
.min(1)
|
|
10
|
-
.refine(
|
|
11
|
-
(value) => {
|
|
12
|
-
try {
|
|
13
|
-
if (!value.startsWith("v")) return false;
|
|
14
|
-
|
|
15
|
-
const [, version] = value.split("v");
|
|
16
|
-
if (!version) return false;
|
|
17
|
-
|
|
18
|
-
const [major, minor, patch] = version.split(".");
|
|
19
|
-
if (!(major && minor && patch)) return false;
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
!(
|
|
23
|
-
Number.isInteger(Number(major)) &&
|
|
24
|
-
Number.isInteger(Number(minor)) &&
|
|
25
|
-
Number.isInteger(Number(patch))
|
|
26
|
-
)
|
|
27
|
-
) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
7
|
+
export const PackageVersionError = { error: "package.version.error" } as const;
|
|
30
8
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
{ message: "package.version.error" },
|
|
37
|
-
)
|
|
9
|
+
export const PackageVersionValue = z
|
|
10
|
+
.string(PackageVersionError)
|
|
11
|
+
.regex(/^v(\d+)\.(\d+)\.(\d+)$/, PackageVersionError)
|
|
38
12
|
.transform((value) => {
|
|
39
|
-
const
|
|
13
|
+
const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(value)!;
|
|
40
14
|
|
|
41
|
-
const
|
|
15
|
+
const major = Number(match[1]);
|
|
16
|
+
const minor = Number(match[2]);
|
|
17
|
+
const patch = Number(match[3]);
|
|
42
18
|
|
|
43
|
-
return {
|
|
44
|
-
major: Number(major),
|
|
45
|
-
minor: Number(minor),
|
|
46
|
-
patch: Number(patch),
|
|
47
|
-
};
|
|
19
|
+
return { major, minor, patch };
|
|
48
20
|
})
|
|
49
21
|
.brand("PackageVersionValue");
|
|
22
|
+
|
|
50
23
|
export type PackageVersionValueType = z.infer<typeof PackageVersionValue>;
|
|
51
24
|
|
|
52
25
|
export class PackageVersion {
|
|
@@ -56,7 +29,7 @@ export class PackageVersion {
|
|
|
56
29
|
readonly patch: PatchType,
|
|
57
30
|
) {}
|
|
58
31
|
|
|
59
|
-
isGreaterThanOrEqual(another: PackageVersion) {
|
|
32
|
+
isGreaterThanOrEqual(another: PackageVersion): boolean {
|
|
60
33
|
if (this.major > another.major) return true;
|
|
61
34
|
if (this.major < another.major) return false;
|
|
62
35
|
|
|
@@ -69,19 +42,17 @@ export class PackageVersion {
|
|
|
69
42
|
return true;
|
|
70
43
|
}
|
|
71
44
|
|
|
72
|
-
toString() {
|
|
45
|
+
toString(): string {
|
|
73
46
|
return `${this.major}.${this.minor}.${this.patch}`;
|
|
74
47
|
}
|
|
75
48
|
|
|
76
|
-
static fromStringWithV(value: string) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
return new PackageVersion(version.major, version.minor, version.patch);
|
|
49
|
+
static fromStringWithV(value: string): PackageVersion {
|
|
50
|
+
const parsed = PackageVersionValue.parse(value);
|
|
51
|
+
return new PackageVersion(parsed.major, parsed.minor, parsed.patch);
|
|
80
52
|
}
|
|
81
53
|
|
|
82
|
-
static fromString(value: string) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
return new PackageVersion(version.major, version.minor, version.patch);
|
|
54
|
+
static fromString(value: string): PackageVersion {
|
|
55
|
+
const parsed = PackageVersionValue.parse(`v${value}`);
|
|
56
|
+
return new PackageVersion(parsed.major, parsed.minor, parsed.patch);
|
|
86
57
|
}
|
|
87
58
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const PaginationTakeError = { error: "pagination.take.invalid" } as const;
|
|
4
|
+
const PaginationSkipError = { error: "pagination.skip.invalid" } as const;
|
|
5
|
+
const PaginationPageError = { error: "pagination.page.invalid" } as const;
|
|
6
|
+
|
|
7
|
+
const Take = z.number(PaginationTakeError).int(PaginationTakeError).gte(1, PaginationTakeError);
|
|
8
|
+
|
|
4
9
|
type TakeType = z.infer<typeof Take>;
|
|
5
10
|
|
|
6
|
-
const Skip = z.number().int().gte(0);
|
|
11
|
+
const Skip = z.number(PaginationSkipError).int(PaginationSkipError).gte(0, PaginationSkipError);
|
|
12
|
+
|
|
7
13
|
type SkipType = z.infer<typeof Skip>;
|
|
8
14
|
|
|
9
15
|
const Page = z.coerce
|
|
10
|
-
.number()
|
|
11
|
-
.int()
|
|
16
|
+
.number(PaginationPageError)
|
|
17
|
+
.int(PaginationPageError)
|
|
12
18
|
.transform((value) => (value <= 0 ? 1 : value))
|
|
13
19
|
.default(1);
|
|
20
|
+
|
|
14
21
|
export type PageType = z.infer<typeof Page>;
|
|
15
22
|
|
|
16
23
|
export type PaginationType = { values: { take: TakeType; skip: SkipType }; page: PageType };
|
|
@@ -46,16 +53,11 @@ export class Pagination {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
static isExhausted(config: PaginationExhaustedConfig): ExhaustedType {
|
|
49
|
-
|
|
50
|
-
const currentPage = config.pagination.page;
|
|
51
|
-
|
|
52
|
-
return lastPage <= currentPage;
|
|
56
|
+
return Pagination.getLastPage(config) <= config.pagination.page;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
private static getLastPage(config: PaginationExhaustedConfig): PageType {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return Page.parse(lastPage);
|
|
60
|
+
return Page.parse(Math.ceil(config.total / config.pagination.values.take));
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
static empty = {
|
|
@@ -70,8 +72,8 @@ export class Pagination {
|
|
|
70
72
|
},
|
|
71
73
|
};
|
|
72
74
|
|
|
73
|
-
static getFirstPage(
|
|
74
|
-
return { values: { take, skip: Skip.parse(0) }, page: Page.parse(1) };
|
|
75
|
+
static getFirstPage(input: { take: TakeType }): PaginationType {
|
|
76
|
+
return { values: { take: Take.parse(input.take), skip: Skip.parse(0) }, page: Page.parse(1) };
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
package/src/quarter-iso-id.vo.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
+
export const QuarterIsoIdError = { error: "quarter-iso-id.invalid" } as const;
|
|
4
|
+
|
|
3
5
|
export const QuarterIsoId = z
|
|
4
|
-
.string()
|
|
5
|
-
.regex(/^\d{4}-Q[1-4]
|
|
6
|
-
.refine(
|
|
7
|
-
(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
.string(QuarterIsoIdError)
|
|
7
|
+
.regex(/^\d{4}-Q[1-4]$/, QuarterIsoIdError)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
const [yearPart, quarterPart] = value.split("-Q");
|
|
10
|
+
const year = Number(yearPart);
|
|
11
|
+
const quarter = Number(quarterPart);
|
|
12
|
+
|
|
13
|
+
return Number.isInteger(year) && Number.isInteger(quarter) && quarter >= 1 && quarter <= 4;
|
|
14
|
+
}, QuarterIsoIdError);
|
|
11
15
|
|
|
12
|
-
return (
|
|
13
|
-
y.length === 4 && Number.isInteger(year) && Number.isInteger(quarter) && quarter >= 1 && quarter <= 4
|
|
14
|
-
);
|
|
15
|
-
},
|
|
16
|
-
{ message: "quarter-iso-id.invalid" },
|
|
17
|
-
);
|
|
18
16
|
export type QuarterIsoIdType = z.infer<typeof QuarterIsoId>;
|
package/src/quarter.vo.ts
CHANGED
|
@@ -7,12 +7,14 @@ export class Quarter extends DateRange {
|
|
|
7
7
|
toIsoId(): QuarterIsoIdType {
|
|
8
8
|
const year = getYear(this.getStart());
|
|
9
9
|
const quarter = getQuarter(this.getStart());
|
|
10
|
+
|
|
10
11
|
return `${year}-Q${quarter}` as QuarterIsoIdType;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
static fromTimestamp(timestamp: TimestampType): Quarter {
|
|
14
15
|
const start = Timestamp.parse(startOfQuarter(timestamp).getTime());
|
|
15
16
|
const end = Timestamp.parse(endOfQuarter(timestamp).getTime());
|
|
17
|
+
|
|
16
18
|
return new Quarter(start, end);
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -23,6 +25,7 @@ export class Quarter extends DateRange {
|
|
|
23
25
|
static fromIsoId(isoId: QuarterIsoIdType): Quarter {
|
|
24
26
|
const [year, quarter] = QuarterIsoId.parse(isoId).split("-Q").map(Number);
|
|
25
27
|
const reference = setQuarter(new Date(Date.UTC(year, 0, 1)), quarter);
|
|
28
|
+
|
|
26
29
|
return Quarter.fromTimestamp(Timestamp.parse(reference.getTime()));
|
|
27
30
|
}
|
|
28
31
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import type { TimeResult } from "./time.service";
|
|
2
1
|
import { Timestamp, type TimestampType } from "./timestamp.vo";
|
|
3
|
-
import type { Falsy } from "./ts-utils";
|
|
4
2
|
|
|
5
|
-
type RateLimiterOptionsType =
|
|
3
|
+
type RateLimiterOptionsType = { ms: TimestampType };
|
|
6
4
|
type RateLimiterResultSuccessType = { allowed: true };
|
|
7
5
|
type RateLimiterResultErrorType = { allowed: false; remainingMs: TimestampType };
|
|
8
6
|
type RateLimiterResultType = RateLimiterResultSuccessType | RateLimiterResultErrorType;
|
|
9
7
|
|
|
10
8
|
export class RateLimiter {
|
|
11
|
-
private lastInvocationTimestampMs:
|
|
9
|
+
private lastInvocationTimestampMs: TimestampType | null = null;
|
|
12
10
|
|
|
13
11
|
constructor(private readonly options: RateLimiterOptionsType) {}
|
|
14
12
|
|
|
15
13
|
verify(currentTimestampMs: TimestampType): RateLimiterResultType {
|
|
16
|
-
if (this.lastInvocationTimestampMs
|
|
14
|
+
if (this.lastInvocationTimestampMs == null) {
|
|
17
15
|
this.lastInvocationTimestampMs = currentTimestampMs;
|
|
18
16
|
|
|
19
17
|
return { allowed: true };
|
|
@@ -23,10 +21,12 @@ export class RateLimiter {
|
|
|
23
21
|
|
|
24
22
|
if (nextAllowedTimestampMs <= currentTimestampMs) {
|
|
25
23
|
this.lastInvocationTimestampMs = currentTimestampMs;
|
|
26
|
-
|
|
27
24
|
return { allowed: true };
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
const remainingDelta = nextAllowedTimestampMs - currentTimestampMs;
|
|
28
|
+
const remainingMs = Timestamp.parse(remainingDelta);
|
|
29
|
+
|
|
30
|
+
return { allowed: false, remainingMs };
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import { DoublyLinkedList, Node } from "./dll.service";
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const ReorderingPositionError = { error: "reordering.position.invalid" } as const;
|
|
5
|
+
export const ReorderingCannotFindItemError = { error: "reordering.item.not_found" } as const;
|
|
6
|
+
export const ReorderingCannotFindCurrentError = { error: "reordering.current_item.not_found" } as const;
|
|
7
|
+
export const ReorderingCannotFindTargetError = { error: "reordering.target_item.not_found" } as const;
|
|
8
|
+
export const ReorderingCorrelationIdError = { error: "reordering.correlation_id.invalid" } as const;
|
|
9
|
+
export const ReorderingItemIdError = { error: "reordering.item_id.invalid" } as const;
|
|
10
|
+
|
|
11
|
+
export const ReorderingItemPositionValue = z
|
|
12
|
+
.number(ReorderingPositionError)
|
|
13
|
+
.int(ReorderingPositionError)
|
|
14
|
+
.min(0, ReorderingPositionError);
|
|
5
15
|
export type ReorderingItemPositionValueType = z.infer<typeof ReorderingItemPositionValue>;
|
|
6
16
|
|
|
7
|
-
export const ReorderingCorrelationId = z
|
|
17
|
+
export const ReorderingCorrelationId = z
|
|
18
|
+
.string(ReorderingCorrelationIdError)
|
|
19
|
+
.min(1, ReorderingCorrelationIdError);
|
|
8
20
|
export type ReorderingCorrelationIdType = z.infer<typeof ReorderingCorrelationId>;
|
|
9
21
|
|
|
10
|
-
export const ReorderingItemId = z.
|
|
22
|
+
export const ReorderingItemId = z.string(ReorderingItemIdError);
|
|
11
23
|
export type ReorderingItemIdType = z.infer<typeof ReorderingItemId>;
|
|
12
24
|
|
|
13
25
|
export const Reordering = z.object({
|
|
@@ -15,7 +27,6 @@ export const Reordering = z.object({
|
|
|
15
27
|
id: ReorderingItemId,
|
|
16
28
|
position: ReorderingItemPositionValue,
|
|
17
29
|
});
|
|
18
|
-
|
|
19
30
|
export type ReorderingType = z.infer<typeof Reordering>;
|
|
20
31
|
|
|
21
32
|
export type WithReorderingPositionValue<T> = T & { position: ReorderingItemPositionValueType };
|
|
@@ -24,9 +35,8 @@ export class ReorderingPosition {
|
|
|
24
35
|
readonly value: ReorderingItemPositionValueType;
|
|
25
36
|
|
|
26
37
|
constructor(value: ReorderingItemPositionValueType) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
38
|
+
const parsed = ReorderingItemPositionValue.safeParse(value);
|
|
39
|
+
if (!parsed.success) throw new Error(ReorderingPositionError.error);
|
|
30
40
|
|
|
31
41
|
this.value = value;
|
|
32
42
|
}
|
|
@@ -55,21 +65,19 @@ enum ReorderingTransferDirection {
|
|
|
55
65
|
|
|
56
66
|
export class ReorderingTransfer {
|
|
57
67
|
readonly id: ReorderingItem["id"];
|
|
58
|
-
|
|
59
68
|
readonly to: ReorderingPosition;
|
|
60
69
|
|
|
61
70
|
constructor(config: { id: ReorderingItem["id"]; to: ReorderingItemPositionValueType }) {
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
const id = config.id;
|
|
72
|
+
const to = new ReorderingPosition(config.to);
|
|
73
|
+
|
|
74
|
+
this.id = id;
|
|
75
|
+
this.to = to;
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
getDirection(currentPosition: ReorderingPosition): ReorderingTransferDirection {
|
|
67
79
|
if (this.to.value === currentPosition.value) return ReorderingTransferDirection.noop;
|
|
68
|
-
|
|
69
|
-
if (this.to.value > currentPosition.value) {
|
|
70
|
-
return ReorderingTransferDirection.downwards;
|
|
71
|
-
}
|
|
72
|
-
|
|
80
|
+
if (this.to.value > currentPosition.value) return ReorderingTransferDirection.downwards;
|
|
73
81
|
return ReorderingTransferDirection.upwards;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -90,62 +98,68 @@ export class ReorderingCalculator {
|
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
add(id: ReorderingItem["id"]): ReorderingItem {
|
|
93
|
-
const
|
|
101
|
+
const size = this.dll.getSize();
|
|
102
|
+
const position = new ReorderingPosition(ReorderingItemPositionValue.parse(size));
|
|
94
103
|
const item = new ReorderingItem(id, position);
|
|
95
|
-
|
|
104
|
+
const node = new Node(item);
|
|
105
|
+
this.dll.append(node);
|
|
96
106
|
return item;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
delete(id: ReorderingItem["id"]) {
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
110
|
+
const node = this.dll.find((x) => x.data.eq(id));
|
|
111
|
+
if (!node) throw new Error(ReorderingCannotFindItemError.error);
|
|
102
112
|
|
|
103
|
-
this.dll.remove(
|
|
113
|
+
this.dll.remove(node);
|
|
104
114
|
this.recalculate();
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
transfer(transfer: ReorderingTransfer): ReturnType<ReorderingCalculator["read"]> {
|
|
108
118
|
const current = this.dll.find((node) => node.data.eq(transfer.id));
|
|
109
|
-
|
|
119
|
+
if (!current) throw new Error(ReorderingCannotFindCurrentError.error);
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
if (!target) throw new Error(
|
|
121
|
+
const target = this.dll.find((node) => node.data.position.eq(transfer.to));
|
|
122
|
+
if (!target) throw new Error(ReorderingCannotFindTargetError.error);
|
|
113
123
|
|
|
114
124
|
const direction = transfer.getDirection(current.data.position);
|
|
115
|
-
|
|
116
125
|
if (direction === ReorderingTransferDirection.noop) return this.read();
|
|
126
|
+
|
|
127
|
+
// remove first to avoid temporary invalid duplicates of positions
|
|
128
|
+
this.dll.remove(current);
|
|
129
|
+
|
|
117
130
|
if (direction === ReorderingTransferDirection.upwards) {
|
|
118
|
-
this.dll.remove(current);
|
|
119
131
|
this.dll.insertBefore(current, target);
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
if (direction === ReorderingTransferDirection.downwards) {
|
|
123
|
-
this.dll.remove(current);
|
|
132
|
+
} else {
|
|
124
133
|
this.dll.insertAfter(current, target);
|
|
125
|
-
this.recalculate();
|
|
126
134
|
}
|
|
135
|
+
|
|
136
|
+
this.recalculate();
|
|
127
137
|
return this.read();
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
read() {
|
|
131
|
-
const ids = Array.from(this.dll).map((
|
|
132
|
-
const items = Array.from(this.dll).map((
|
|
141
|
+
const ids = Array.from(this.dll).map((node) => node.data.id);
|
|
142
|
+
const items = Array.from(this.dll).map((node) => node.data);
|
|
133
143
|
return { ids, items };
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
private recalculate() {
|
|
137
|
-
|
|
147
|
+
let index = 0;
|
|
148
|
+
for (const node of this.dll) {
|
|
149
|
+
const id = node.data.id;
|
|
150
|
+
const position = new ReorderingPosition(index);
|
|
151
|
+
node.data = new ReorderingItem(id, position);
|
|
152
|
+
index += 1;
|
|
153
|
+
}
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
export class ReorderingIntegrator {
|
|
142
158
|
static appendPosition(reordering: ReorderingType[]) {
|
|
143
159
|
return function <T extends { id: ReorderingItemIdType }>(item: T): WithReorderingPositionValue<T> {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return { ...item, position };
|
|
160
|
+
const found = reordering.find((x) => x.id === item.id);
|
|
161
|
+
const positionValue = ReorderingItemPositionValue.parse(found?.position ?? 0);
|
|
162
|
+
return { ...item, position: positionValue };
|
|
149
163
|
};
|
|
150
164
|
}
|
|
151
165
|
|
package/src/revision.vo.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import type { ETag, WeakETag } from "./etags.vo";
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const RevisionValueError = { error: "invalid.revision.value" } as const;
|
|
5
|
+
|
|
6
|
+
export const RevisionValue = z
|
|
7
|
+
.number(RevisionValueError)
|
|
8
|
+
.int(RevisionValueError)
|
|
9
|
+
.min(0, RevisionValueError)
|
|
10
|
+
.brand("RevisionValue");
|
|
11
|
+
|
|
5
12
|
export type RevisionValueType = z.infer<typeof RevisionValue>;
|
|
6
13
|
|
|
14
|
+
export const RevisionInvalidErrorMessage = "revision.invalid" as const;
|
|
15
|
+
export const RevisionMismatchErrorMessage = "revision.mismatch" as const;
|
|
16
|
+
|
|
7
17
|
export class Revision {
|
|
18
|
+
static readonly INITIAL: RevisionValueType = RevisionValue.parse(0);
|
|
19
|
+
|
|
8
20
|
readonly value: RevisionValueType;
|
|
9
|
-
static initial: RevisionValueType = 0;
|
|
10
21
|
|
|
11
22
|
constructor(value: unknown) {
|
|
12
23
|
const result = RevisionValue.safeParse(value);
|
|
13
|
-
|
|
14
24
|
if (!result.success) throw new InvalidRevisionError();
|
|
15
|
-
|
|
16
25
|
this.value = result.data;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
equals(another: RevisionValueType): boolean {
|
|
20
29
|
return this.value === another;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
validate(another: RevisionValueType): void {
|
|
24
|
-
if (!this.
|
|
33
|
+
if (!this.equals(another)) throw new RevisionMismatchError();
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
next(): Revision {
|
|
@@ -41,14 +50,14 @@ export class Revision {
|
|
|
41
50
|
|
|
42
51
|
export class RevisionMismatchError extends Error {
|
|
43
52
|
constructor() {
|
|
44
|
-
super();
|
|
53
|
+
super(RevisionMismatchErrorMessage);
|
|
45
54
|
Object.setPrototypeOf(this, RevisionMismatchError.prototype);
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export class InvalidRevisionError extends Error {
|
|
50
59
|
constructor() {
|
|
51
|
-
super();
|
|
60
|
+
super(RevisionInvalidErrorMessage);
|
|
52
61
|
Object.setPrototypeOf(this, InvalidRevisionError.prototype);
|
|
53
62
|
}
|
|
54
63
|
}
|
package/src/rounding.adapter.ts
CHANGED
|
@@ -22,9 +22,7 @@ export const RoundingDecimalsError = "invalid.rounding.decimals" as const;
|
|
|
22
22
|
|
|
23
23
|
export class RoundToDecimal implements RoundingPort {
|
|
24
24
|
constructor(private readonly decimals: number) {
|
|
25
|
-
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 100)
|
|
26
|
-
throw new Error(RoundingDecimalsError);
|
|
27
|
-
}
|
|
25
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 100) throw new Error(RoundingDecimalsError);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
round(value: number): number {
|
package/src/size.vo.ts
CHANGED
|
@@ -15,17 +15,15 @@ type SizeConfigType = { unit: SizeUnit; value: number };
|
|
|
15
15
|
|
|
16
16
|
export class Size {
|
|
17
17
|
private readonly unit: SizeUnit;
|
|
18
|
-
|
|
19
18
|
private readonly value: SizeValueType;
|
|
20
|
-
|
|
21
19
|
private readonly bytes: SizeValueType;
|
|
22
20
|
|
|
23
21
|
private static readonly KB_MULTIPLIER = 1024;
|
|
24
|
-
|
|
25
22
|
private static readonly MB_MULTIPLIER = 1024 * Size.KB_MULTIPLIER;
|
|
26
|
-
|
|
27
23
|
private static readonly GB_MULTIPLIER = 1024 * Size.MB_MULTIPLIER;
|
|
28
24
|
|
|
25
|
+
private static readonly ROUNDER = new RoundToDecimal(2);
|
|
26
|
+
|
|
29
27
|
constructor(config: SizeConfigType) {
|
|
30
28
|
this.unit = config.unit;
|
|
31
29
|
this.value = SizeValue.parse(config.value);
|
|
@@ -60,28 +58,20 @@ export class Size {
|
|
|
60
58
|
return this.bytes;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
isGreaterThan(another: Size) {
|
|
61
|
+
isGreaterThan(another: Size): boolean {
|
|
64
62
|
return this.bytes > another.toBytes();
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
format(unit: SizeUnit): string {
|
|
68
|
-
const rounding = new RoundToDecimal(2);
|
|
69
|
-
|
|
70
66
|
switch (unit) {
|
|
71
67
|
case SizeUnit.kB: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return `${kbs} ${SizeUnit.kB}`;
|
|
68
|
+
return `${Size.ROUNDER.round(this.bytes / Size.KB_MULTIPLIER)} ${SizeUnit.kB}`;
|
|
75
69
|
}
|
|
76
70
|
case SizeUnit.MB: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return `${mbs} ${SizeUnit.MB}`;
|
|
71
|
+
return `${Size.ROUNDER.round(this.bytes / Size.MB_MULTIPLIER)} ${SizeUnit.MB}`;
|
|
80
72
|
}
|
|
81
73
|
case SizeUnit.GB: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return `${gbs} ${SizeUnit.GB}`;
|
|
74
|
+
return `${Size.ROUNDER.round(this.bytes / Size.GB_MULTIPLIER)} ${SizeUnit.GB}`;
|
|
85
75
|
}
|
|
86
76
|
default: {
|
|
87
77
|
// SizeUnit.b
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { format,
|
|
1
|
+
import { format, subDays } from "date-fns";
|
|
2
2
|
|
|
3
3
|
type DateType = string;
|
|
4
4
|
export type StreakType = { cutoff: DateType; dates: DateType[]; streak: number };
|
|
@@ -6,32 +6,27 @@ export type StreakType = { cutoff: DateType; dates: DateType[]; streak: number }
|
|
|
6
6
|
export class StreakCalculator {
|
|
7
7
|
private readonly cutoff: DateType;
|
|
8
8
|
|
|
9
|
-
constructor() {
|
|
10
|
-
|
|
11
|
-
this.cutoff = StreakCalculator.format(today);
|
|
9
|
+
constructor(now: Date = new Date()) {
|
|
10
|
+
this.cutoff = StreakCalculator.format(now);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
calculate(
|
|
15
|
-
const dates = Array.from(new Set(
|
|
13
|
+
calculate(inputDates: DateType[]): StreakType {
|
|
14
|
+
const dates = Array.from(new Set(inputDates));
|
|
15
|
+
const datesSet = new Set(dates);
|
|
16
16
|
|
|
17
17
|
let streak = 0;
|
|
18
|
-
let
|
|
18
|
+
let cursor = this.cutoff;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (isEqual(streakTail, date) || isAfter(date, streakTail)) {
|
|
26
|
-
streakTail = StreakCalculator.format(subDays(date, 1));
|
|
27
|
-
streak++;
|
|
28
|
-
} else break;
|
|
20
|
+
while (datesSet.has(cursor)) {
|
|
21
|
+
streak++;
|
|
22
|
+
const cursorDate = new Date(`${cursor}T00:00:00Z`);
|
|
23
|
+
cursor = StreakCalculator.format(subDays(cursorDate, 1));
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
return { cutoff: this.cutoff, dates, streak };
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
static format(date:
|
|
29
|
+
static format(date: Date | number): DateType {
|
|
35
30
|
return format(date, "yyyy-MM-dd");
|
|
36
31
|
}
|
|
37
32
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const TimeZoneOffsetValue = z
|
|
4
|
-
|
|
5
|
-
.trim()
|
|
6
|
-
.or(z.undefined())
|
|
7
|
-
.transform((value) => Number(value))
|
|
8
|
-
.transform((value) => (Number.isNaN(value) ? 0 : value))
|
|
9
|
-
.brand("TimeZoneOffsetValue");
|
|
3
|
+
export const TimeZoneOffsetValue = z.coerce.number().catch(0).brand("TimeZoneOffsetValue");
|
|
4
|
+
|
|
10
5
|
export type TimeZoneOffsetValueType = z.infer<typeof TimeZoneOffsetValue>;
|