@bgord/tools 0.17.2 → 1.0.1
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/age-years.vo.d.ts +11 -0
- package/dist/age-years.vo.js +9 -0
- package/dist/age.vo.d.ts +11 -16
- package/dist/age.vo.js +20 -31
- package/dist/api-key.vo.d.ts +3 -1
- package/dist/api-key.vo.js +10 -5
- package/dist/basename.vo.d.ts +9 -9
- package/dist/basename.vo.js +22 -22
- package/dist/clock.vo.d.ts +10 -4
- package/dist/clock.vo.js +12 -14
- package/dist/date-calculator.service.d.ts +2 -1
- package/dist/date-formatter.service.d.ts +3 -4
- package/dist/date-range.vo.d.ts +7 -1
- package/dist/date-range.vo.js +5 -2
- package/dist/day-iso-id.vo.d.ts +5 -2
- package/dist/day-iso-id.vo.js +11 -7
- package/dist/day.vo.d.ts +4 -3
- package/dist/day.vo.js +18 -16
- package/dist/directory-path-absolute.vo.d.ts +10 -6
- package/dist/directory-path-absolute.vo.js +19 -17
- package/dist/directory-path-relative.vo.d.ts +10 -7
- package/dist/directory-path-relative.vo.js +18 -17
- package/dist/division-factor.vo.d.ts +7 -0
- package/dist/division-factor.vo.js +9 -0
- package/dist/duration-ms.vo.d.ts +6 -0
- package/dist/duration-ms.vo.js +3 -0
- package/dist/duration.service.d.ts +2 -14
- package/dist/duration.service.js +16 -35
- package/dist/email-mask.service.d.ts +1 -6
- package/dist/email-mask.service.js +6 -8
- package/dist/etags.vo.d.ts +4 -3
- package/dist/etags.vo.js +3 -3
- package/dist/extension.vo.d.ts +6 -4
- package/dist/extension.vo.js +15 -10
- package/dist/feature-flag-value.vo.d.ts +10 -0
- package/dist/feature-flag-value.vo.js +8 -0
- package/dist/feature-flag.vo.d.ts +1 -7
- package/dist/feature-flag.vo.js +1 -7
- package/dist/file-path-absolute-schema.vo.d.ts +10 -7
- package/dist/file-path-absolute-schema.vo.js +17 -17
- package/dist/file-path-relative-schema.vo.d.ts +10 -7
- package/dist/file-path-relative-schema.vo.js +14 -12
- package/dist/file-path.vo.d.ts +4 -4
- package/dist/file-path.vo.js +8 -8
- package/dist/filename-from-string.vo.d.ts +4 -2
- package/dist/filename-from-string.vo.js +10 -8
- package/dist/filename-suffix.vo.d.ts +7 -3
- package/dist/filename-suffix.vo.js +13 -7
- package/dist/filename.vo.d.ts +2 -0
- package/dist/filename.vo.js +8 -2
- package/dist/height-milimiters.vo.d.ts +6 -0
- package/dist/height-milimiters.vo.js +10 -0
- package/dist/height.vo.d.ts +3 -20
- package/dist/height.vo.js +11 -62
- package/dist/hour-format.service.js +1 -1
- package/dist/hour-schema.vo.d.ts +7 -0
- package/dist/hour-schema.vo.js +8 -0
- package/dist/hour.vo.d.ts +4 -3
- package/dist/hour.vo.js +8 -8
- package/dist/iban-mask.service.d.ts +1 -3
- package/dist/iban-mask.service.js +2 -2
- package/dist/iban-schema.vo.d.ts +7 -0
- package/dist/iban-schema.vo.js +10 -0
- package/dist/iban.vo.d.ts +4 -10
- package/dist/iban.vo.js +6 -13
- package/dist/image.vo.d.ts +6 -4
- package/dist/image.vo.js +13 -12
- package/dist/index.d.ts +24 -2
- package/dist/index.js +24 -2
- package/dist/language.vo.d.ts +2 -1
- package/dist/language.vo.js +6 -4
- package/dist/linear-regression.service.d.ts +27 -0
- package/dist/{simple-linear-regression.service.js → linear-regression.service.js} +17 -15
- package/dist/mean.service.d.ts +3 -1
- package/dist/mean.service.js +3 -4
- package/dist/mime-types.vo.d.ts +1 -2
- package/dist/mime-value.vo.d.ts +9 -0
- package/dist/mime-value.vo.js +9 -0
- package/dist/mime.vo.d.ts +11 -17
- package/dist/mime.vo.js +10 -27
- package/dist/min-max-scaler.service.d.ts +7 -5
- package/dist/min-max-scaler.service.js +12 -10
- package/dist/minute-schema.vo.d.ts +7 -0
- package/dist/minute-schema.vo.js +8 -0
- package/dist/minute.vo.d.ts +4 -3
- package/dist/minute.vo.js +8 -8
- package/dist/money-amount.vo.d.ts +7 -0
- package/dist/money-amount.vo.js +7 -0
- package/dist/money.vo.d.ts +9 -18
- package/dist/money.vo.js +14 -27
- package/dist/month-iso-id.vo.d.ts +4 -2
- package/dist/month-iso-id.vo.js +13 -7
- package/dist/month.vo.d.ts +4 -3
- package/dist/month.vo.js +21 -21
- package/dist/multiplication-factor.vo.d.ts +7 -0
- package/dist/multiplication-factor.vo.js +9 -0
- package/dist/object-key.vo.d.ts +9 -6
- package/dist/object-key.vo.js +20 -19
- package/dist/outlier-detector.service.d.ts +3 -1
- package/dist/outlier-detector.service.js +2 -2
- package/dist/package-version-schema.vo.d.ts +11 -0
- package/dist/package-version-schema.vo.js +15 -0
- package/dist/package-version.vo.d.ts +11 -20
- package/dist/package-version.vo.js +11 -20
- package/dist/pagination-page.vo.d.ts +6 -0
- package/dist/pagination-page.vo.js +7 -0
- package/dist/pagination-skip.vo.d.ts +7 -0
- package/dist/pagination-skip.vo.js +9 -0
- package/dist/pagination-take.vo.d.ts +7 -0
- package/dist/pagination-take.vo.js +9 -0
- package/dist/pagination.service.d.ts +3 -8
- package/dist/pagination.service.js +5 -12
- package/dist/percentage.service.d.ts +3 -1
- package/dist/percentage.service.js +2 -2
- package/dist/population-standard-deviation.service.d.ts +3 -1
- package/dist/population-standard-deviation.service.js +5 -4
- package/dist/quarter-iso-id.vo.d.ts +3 -2
- package/dist/quarter-iso-id.vo.js +7 -9
- package/dist/quarter.vo.d.ts +2 -1
- package/dist/quarter.vo.js +10 -7
- package/dist/random.service.d.ts +3 -4
- package/dist/random.service.js +5 -11
- package/dist/rate-limiter.service.d.ts +2 -2
- package/dist/rate-limiter.service.js +8 -8
- package/dist/reordering-item-position-value.vo.d.ts +6 -0
- package/dist/reordering-item-position-value.vo.js +6 -0
- package/dist/reordering.service.d.ts +7 -23
- package/dist/reordering.service.js +15 -24
- package/dist/revision-value.vo.d.ts +7 -0
- package/dist/revision-value.vo.js +6 -0
- package/dist/revision.vo.d.ts +6 -13
- package/dist/revision.vo.js +10 -22
- package/dist/rounding.adapter.d.ts +7 -2
- package/dist/rounding.adapter.js +13 -5
- package/dist/size-bytes.vo.d.ts +6 -0
- package/dist/size-bytes.vo.js +7 -0
- package/dist/size.vo.d.ts +15 -15
- package/dist/size.vo.js +41 -51
- package/dist/stopwatch.service.d.ts +3 -1
- package/dist/stopwatch.service.js +2 -2
- package/dist/sum.service.js +8 -8
- package/dist/thousands-separator.service.js +4 -1
- package/dist/time.service.d.ts +8 -0
- package/dist/time.service.js +13 -0
- package/dist/timestamp.vo.d.ts +1 -1
- package/dist/timestamp.vo.js +4 -5
- package/dist/timezone.vo.d.ts +4 -1
- package/dist/timezone.vo.js +12 -6
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/week-iso-id.vo.d.ts +4 -2
- package/dist/week-iso-id.vo.js +15 -9
- package/dist/week.vo.d.ts +4 -3
- package/dist/week.vo.js +21 -22
- package/dist/weekday.vo.d.ts +1 -1
- package/dist/weekday.vo.js +6 -8
- package/dist/weight-grams.vo.d.ts +7 -0
- package/dist/weight-grams.vo.js +7 -0
- package/dist/weight.vo.d.ts +12 -35
- package/dist/weight.vo.js +23 -72
- package/dist/year-iso-id.vo.d.ts +3 -2
- package/dist/year-iso-id.vo.js +6 -4
- package/dist/year.vo.d.ts +5 -6
- package/dist/year.vo.js +21 -26
- package/dist/z-score.service.d.ts +3 -1
- package/dist/z-score.service.js +2 -2
- package/package.json +4 -4
- package/readme.md +21 -2
- package/src/age-years.vo.ts +14 -0
- package/src/age.vo.ts +22 -35
- package/src/api-key.vo.ts +11 -5
- package/src/basename.vo.ts +24 -22
- package/src/clock.vo.ts +16 -17
- package/src/date-calculator.service.ts +2 -1
- package/src/date-formatter.service.ts +4 -5
- package/src/date-range.vo.ts +6 -2
- package/src/day-iso-id.vo.ts +12 -8
- package/src/day.vo.ts +27 -24
- package/src/directory-path-absolute.vo.ts +23 -18
- package/src/directory-path-relative.vo.ts +21 -18
- package/src/division-factor.vo.ts +13 -0
- package/src/duration-ms.vo.ts +7 -0
- package/src/duration.service.ts +16 -40
- package/src/email-mask.service.ts +7 -15
- package/src/etags.vo.ts +4 -5
- package/src/extension.vo.ts +17 -10
- package/src/feature-flag-value.vo.ts +12 -0
- package/src/feature-flag.vo.ts +1 -9
- package/src/file-path-absolute-schema.vo.ts +18 -17
- package/src/file-path-relative-schema.vo.ts +15 -12
- package/src/file-path.vo.ts +8 -8
- package/src/filename-from-string.vo.ts +12 -9
- package/src/filename-suffix.vo.ts +14 -7
- package/src/filename.vo.ts +11 -2
- package/src/height-milimiters.vo.ts +12 -0
- package/src/height.vo.ts +12 -83
- package/src/hour-format.service.ts +2 -1
- package/src/hour-schema.vo.ts +12 -0
- package/src/hour.vo.ts +12 -12
- package/src/iban-mask.service.ts +3 -5
- package/src/iban-schema.vo.ts +15 -0
- package/src/iban.vo.ts +9 -22
- package/src/image.vo.ts +14 -12
- package/src/index.ts +24 -2
- package/src/language.vo.ts +7 -4
- package/src/linear-regression.service.ts +71 -0
- package/src/mean.service.ts +3 -5
- package/src/mime-types.vo.ts +1 -3
- package/src/mime-value.vo.ts +12 -0
- package/src/mime.vo.ts +12 -33
- package/src/min-max-scaler.service.ts +13 -11
- package/src/minute-schema.vo.ts +12 -0
- package/src/minute.vo.ts +12 -12
- package/src/money-amount.vo.ts +11 -0
- package/src/money.vo.ts +20 -38
- package/src/month-iso-id.vo.ts +14 -7
- package/src/month.vo.ts +25 -24
- package/src/multiplication-factor.vo.ts +13 -0
- package/src/object-key.vo.ts +25 -21
- package/src/outlier-detector.service.ts +2 -2
- package/src/package-version-schema.vo.ts +21 -0
- package/src/package-version.vo.ts +17 -33
- package/src/pagination-page.vo.ts +11 -0
- package/src/pagination-skip.vo.ts +13 -0
- package/src/pagination-take.vo.ts +13 -0
- package/src/pagination.service.ts +5 -22
- package/src/percentage.service.ts +2 -2
- package/src/population-standard-deviation.service.ts +5 -4
- package/src/quarter-iso-id.vo.ts +7 -10
- package/src/quarter.vo.ts +14 -9
- package/src/random.service.ts +6 -9
- package/src/rate-limiter.service.ts +9 -8
- package/src/reordering-item-position-value.vo.ts +10 -0
- package/src/reordering.service.ts +19 -28
- package/src/revision-value.vo.ts +10 -0
- package/src/revision.vo.ts +10 -25
- package/src/rounding.adapter.ts +16 -3
- package/src/size-bytes.vo.ts +11 -0
- package/src/size.vo.ts +43 -54
- package/src/stopwatch.service.ts +3 -3
- package/src/sum.service.ts +8 -8
- package/src/thousands-separator.service.ts +4 -1
- package/src/time.service.ts +15 -0
- package/src/timestamp.vo.ts +4 -5
- package/src/timezone.vo.ts +12 -6
- package/src/week-iso-id.vo.ts +16 -12
- package/src/week.vo.ts +26 -28
- package/src/weekday.vo.ts +6 -9
- package/src/weight-grams.vo.ts +11 -0
- package/src/weight.vo.ts +28 -85
- package/src/year-iso-id.vo.ts +7 -4
- package/src/year.vo.ts +27 -33
- package/src/z-score.service.ts +2 -2
- package/dist/simple-linear-regression.service.d.ts +0 -25
- package/dist/streak-calculator.service.d.ts +0 -13
- package/dist/streak-calculator.service.js +0 -22
- package/src/simple-linear-regression.service.ts +0 -69
- package/src/streak-calculator.service.ts +0 -32
package/src/age.vo.ts
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import { differenceInYears } from "date-fns";
|
|
2
|
-
import {
|
|
2
|
+
import { AgeYears, AgeYearsConstraints, type AgeYearsType } from "./age-years.vo";
|
|
3
3
|
import type { TimestampType } from "./timestamp.vo";
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
export const InvalidBirthdateInFutureError = "invalid.birthdate_in_future" as const;
|
|
7
|
-
export const InvalidBirthdateError = "invalid.birthdate" as const;
|
|
5
|
+
export const AgeError = { FutureBirthdate: "age.future.birthdate" } as const;
|
|
8
6
|
|
|
9
7
|
export class Age {
|
|
10
|
-
static readonly MIN =
|
|
11
|
-
static readonly MAX =
|
|
8
|
+
static readonly MIN = AgeYearsConstraints.min;
|
|
9
|
+
static readonly MAX = AgeYearsConstraints.max;
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
.number(AgeValueError)
|
|
15
|
-
.int(AgeValueError)
|
|
16
|
-
.min(Age.MIN, AgeValueError)
|
|
17
|
-
.max(Age.MAX, AgeValueError)
|
|
18
|
-
.brand("AgeValue");
|
|
11
|
+
private constructor(private readonly value: AgeYearsType) {}
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
static fromValue(candidate: number): Age {
|
|
14
|
+
return new Age(AgeYears.parse(candidate));
|
|
15
|
+
}
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
static fromBirthdateEpochMs(params: { birthdate: TimestampType; now: TimestampType }): Age {
|
|
18
|
+
if (params.birthdate > params.now) throw new Error(AgeError.FutureBirthdate);
|
|
19
|
+
return Age.fromValue(differenceInYears(params.now, params.birthdate));
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
static fromBirthdate(candidate: { birthdate: string; now: TimestampType }): Age {
|
|
23
|
+
const birthdateMs = new Date(candidate.birthdate).getTime();
|
|
24
|
+
|
|
25
|
+
if (birthdateMs > candidate.now) throw new Error(AgeError.FutureBirthdate);
|
|
26
|
+
return Age.fromValue(differenceInYears(candidate.now, birthdateMs));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(): number {
|
|
30
|
+
return this.value;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
equals(other: Age): boolean {
|
|
@@ -43,27 +46,11 @@ export class Age {
|
|
|
43
46
|
return this.value >= minimumAge.value;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static fromBirthdateEpochMs(params: { birthdate: TimestampType; now: TimestampType }): Age {
|
|
51
|
-
if (params.birthdate > params.now) throw new Error(InvalidBirthdateInFutureError);
|
|
52
|
-
return Age.fromValue(differenceInYears(params.now, params.birthdate));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
static fromBirthdate(params: { birthdate: string; now: TimestampType }): Age {
|
|
56
|
-
const birthdateMs = new Date(params.birthdate).getTime();
|
|
57
|
-
|
|
58
|
-
if (birthdateMs > params.now) throw new Error(InvalidBirthdateInFutureError);
|
|
59
|
-
return Age.fromValue(differenceInYears(params.now, birthdateMs));
|
|
49
|
+
toString(): string {
|
|
50
|
+
return this.value.toString();
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
toJSON(): number {
|
|
63
54
|
return this.get();
|
|
64
55
|
}
|
|
65
|
-
|
|
66
|
-
toString(): string {
|
|
67
|
-
return String(this.value);
|
|
68
|
-
}
|
|
69
56
|
}
|
package/src/api-key.vo.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const ApiKeyError = {
|
|
3
|
+
export const ApiKeyError = {
|
|
4
|
+
Type: "api.key.type",
|
|
5
|
+
Length: "api.key.length",
|
|
6
|
+
BadChars: "api.key.bad.chars",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
// 64 letters and digits allowed
|
|
10
|
+
const API_KEY_CHARS = /^[a-zA-Z0-9]{64}$/;
|
|
4
11
|
|
|
5
12
|
export const ApiKey = z
|
|
6
|
-
.string(ApiKeyError)
|
|
7
|
-
.
|
|
8
|
-
.
|
|
9
|
-
.regex(/^[0-9a-zA-Z]{64}$/i, ApiKeyError)
|
|
13
|
+
.string(ApiKeyError.Type)
|
|
14
|
+
.length(64, ApiKeyError.Length)
|
|
15
|
+
.regex(API_KEY_CHARS, ApiKeyError.BadChars)
|
|
10
16
|
.brand("ApiKey");
|
|
11
17
|
|
|
12
18
|
export type ApiKeyType = z.infer<typeof ApiKey>;
|
package/src/basename.vo.ts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
export const BasenameError = {
|
|
4
|
+
Type: "basename.type",
|
|
5
|
+
Empty: "basename.empty",
|
|
6
|
+
TooLong: "basename.too.long",
|
|
7
|
+
DotSegments: "basename.dot.segments",
|
|
8
|
+
Dotfiles: "basename.dotfiles",
|
|
9
|
+
TrailingDot: "basename.trailing.dot",
|
|
10
|
+
BadChars: "basename.bad.chars",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
// Letters, digits, dots, underscores, and hyphens allowed
|
|
14
|
+
const BASENAME_CHARS = /^[a-zA-Z0-9._-]+$/;
|
|
15
|
+
|
|
16
|
+
const DOT_SEGMENTS = [".", ".."];
|
|
12
17
|
|
|
13
18
|
export const Basename = z
|
|
14
|
-
.string(
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.refine((
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
.refine((value) => value
|
|
23
|
-
|
|
24
|
-
.refine((value) => !value.startsWith("."), BasenameDotfilesForbiddenError)
|
|
25
|
-
.refine((value) => !value.endsWith("."), BasenameTrailingDotForbiddenError)
|
|
26
|
-
.regex(/^[A-Za-z0-9._-]+$/, BasenameBadCharsError)
|
|
19
|
+
.string(BasenameError.Type)
|
|
20
|
+
.min(1, BasenameError.Empty)
|
|
21
|
+
.max(128, BasenameError.TooLong)
|
|
22
|
+
// Reject "." and ".." as a filename to avoid directory traversal
|
|
23
|
+
.refine((value) => !DOT_SEGMENTS.includes(value), BasenameError.DotSegments)
|
|
24
|
+
// Reject dotfiles like ".env"
|
|
25
|
+
.refine((value) => !value.startsWith("."), BasenameError.Dotfiles)
|
|
26
|
+
// Reject trailing dot like "picture." to avoid extension collision
|
|
27
|
+
.refine((value) => !value.endsWith("."), BasenameError.TrailingDot)
|
|
28
|
+
.regex(BASENAME_CHARS, BasenameError.BadChars)
|
|
27
29
|
.brand("Basename");
|
|
28
30
|
|
|
29
31
|
export type BasenameType = z.infer<typeof Basename>;
|
package/src/clock.vo.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ClockFormatter, ClockFormatters } from "./clock-format.service";
|
|
2
2
|
import { Hour } from "./hour.vo";
|
|
3
|
+
import type { HourSchemaType } from "./hour-schema.vo";
|
|
3
4
|
import { Minute } from "./minute.vo";
|
|
5
|
+
import type { MinuteSchemaType } from "./minute-schema.vo";
|
|
4
6
|
import type { TimestampType } from "./timestamp.vo";
|
|
5
7
|
|
|
6
8
|
export class Clock {
|
|
@@ -11,7 +13,7 @@ export class Clock {
|
|
|
11
13
|
private readonly minute: Minute,
|
|
12
14
|
formatter?: ClockFormatter,
|
|
13
15
|
) {
|
|
14
|
-
this.formatter =
|
|
16
|
+
this.formatter = formatter ?? ClockFormatters.TWENTY_FOUR_HOURS;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
static fromEpochMs(timestamp: TimestampType, formatter?: ClockFormatter): Clock {
|
|
@@ -21,17 +23,12 @@ export class Clock {
|
|
|
21
23
|
return new Clock(hour, minute, formatter);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
get(): { hour:
|
|
26
|
+
get(): { hour: HourSchemaType; minute: MinuteSchemaType } {
|
|
25
27
|
return { hour: this.hour.get(), minute: this.minute.get() };
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
format(
|
|
29
|
-
|
|
30
|
-
return chosen(this.hour, this.minute);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
toString(): string {
|
|
34
|
-
return this.format();
|
|
30
|
+
format(): string {
|
|
31
|
+
return this.formatter(this.hour, this.minute);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
equals(another: Clock): boolean {
|
|
@@ -39,18 +36,20 @@ export class Clock {
|
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
isAfter(another: Clock): boolean {
|
|
42
|
-
|
|
43
|
-
const otherHour = another.hour.get();
|
|
44
|
-
|
|
45
|
-
if (thisHour !== otherHour) return thisHour > otherHour;
|
|
39
|
+
if (this.hour.get() !== another.hour.get()) return this.hour.get() > another.hour.get();
|
|
46
40
|
return this.minute.get() > another.minute.get();
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
isBefore(another: Clock): boolean {
|
|
50
|
-
|
|
51
|
-
const otherHour = another.hour.get();
|
|
52
|
-
|
|
53
|
-
if (thisHour !== otherHour) return thisHour < otherHour;
|
|
44
|
+
if (this.hour.get() !== another.hour.get()) return this.hour.get() < another.hour.get();
|
|
54
45
|
return this.minute.get() < another.minute.get();
|
|
55
46
|
}
|
|
47
|
+
|
|
48
|
+
toString(): string {
|
|
49
|
+
return this.format();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toJSON(): { hour: HourSchemaType; minute: MinuteSchemaType } {
|
|
53
|
+
return this.get();
|
|
54
|
+
}
|
|
56
55
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Duration } from "./duration.service";
|
|
2
|
+
import type { TimeZoneOffsetValueType } from "./time-zone-offset-value.vo";
|
|
2
3
|
import { Timestamp, type TimestampType } from "./timestamp.vo";
|
|
3
4
|
|
|
4
|
-
type GetStartOfDayTsInTzConfigType = { now: TimestampType; timeZoneOffsetMs:
|
|
5
|
+
type GetStartOfDayTsInTzConfigType = { now: TimestampType; timeZoneOffsetMs: TimeZoneOffsetValueType };
|
|
5
6
|
|
|
6
7
|
export class DateCalculator {
|
|
7
8
|
static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType): TimestampType {
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { format, formatDistanceToNow } from "date-fns";
|
|
2
2
|
|
|
3
|
-
type FormattedDateType = string;
|
|
4
3
|
type DateFormattersInputType = Parameters<typeof format>[0];
|
|
5
4
|
|
|
6
5
|
export class DateFormatters {
|
|
7
|
-
static datetime(date: DateFormattersInputType):
|
|
6
|
+
static datetime(date: DateFormattersInputType): string {
|
|
8
7
|
return format(date, "yyyy/MM/dd HH:mm");
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
static date(date: DateFormattersInputType):
|
|
10
|
+
static date(date: DateFormattersInputType): string {
|
|
12
11
|
return format(date, "yyyy/MM/dd");
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
static monthDay(date: DateFormattersInputType):
|
|
14
|
+
static monthDay(date: DateFormattersInputType): string {
|
|
16
15
|
return format(date, "MM/dd");
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
static relative(date: DateFormattersInputType) {
|
|
18
|
+
static relative(date: DateFormattersInputType): string {
|
|
20
19
|
return formatDistanceToNow(date, { addSuffix: true });
|
|
21
20
|
}
|
|
22
21
|
}
|
package/src/date-range.vo.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { TimestampType } from "./timestamp.vo";
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const DateRangeError = { Invalid: "date.range.invalid" } as const;
|
|
4
4
|
|
|
5
5
|
export class DateRange {
|
|
6
6
|
constructor(
|
|
7
7
|
private readonly start: TimestampType,
|
|
8
8
|
private readonly end: TimestampType,
|
|
9
9
|
) {
|
|
10
|
-
if (start > end) throw new Error(
|
|
10
|
+
if (start > end) throw new Error(DateRangeError.Invalid);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
getStart(): TimestampType {
|
|
@@ -29,4 +29,8 @@ export class DateRange {
|
|
|
29
29
|
equals(other: DateRange): boolean {
|
|
30
30
|
return this.start === other.start && this.end === other.end;
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
toJSON(): { start: number; end: number } {
|
|
34
|
+
return { start: this.getStart(), end: this.getEnd() };
|
|
35
|
+
}
|
|
32
36
|
}
|
package/src/day-iso-id.vo.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { isValid, parseISO } from "date-fns";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
3
|
|
|
4
|
-
export const DayIsoIdError = {
|
|
4
|
+
export const DayIsoIdError = {
|
|
5
|
+
Type: "day.iso.id.type",
|
|
6
|
+
BadChars: "day.iso.id.bad.chars",
|
|
7
|
+
InvalidDate: "day.iso.id.invalid.date",
|
|
8
|
+
} as const;
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.regex(/^\d{4}-\d{2}-\d{2}$/, DayIsoIdError)
|
|
9
|
-
.refine((value) => {
|
|
10
|
-
const date = parseISO(value);
|
|
10
|
+
// Four digits, hyphen, two digits, hyphen, two digits
|
|
11
|
+
export const DAY_ISO_ID_CHARS = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export const DayIsoId = z
|
|
14
|
+
.string(DayIsoIdError.Type)
|
|
15
|
+
.regex(DAY_ISO_ID_CHARS, DayIsoIdError.BadChars)
|
|
16
|
+
.refine((value) => isValid(parseISO(value)), DayIsoIdError.InvalidDate)
|
|
17
|
+
.brand("DayIsoId");
|
|
14
18
|
|
|
15
19
|
export type DayIsoIdType = z.infer<typeof DayIsoId>;
|
package/src/day.vo.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatISO } from "date-fns";
|
|
1
2
|
import { DateRange } from "./date-range.vo";
|
|
2
3
|
import { DayIsoId, type DayIsoIdType } from "./day-iso-id.vo";
|
|
3
4
|
import { Duration } from "./duration.service";
|
|
@@ -8,32 +9,9 @@ export class Day extends DateRange {
|
|
|
8
9
|
super(start, end);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
toIsoId(): DayIsoIdType {
|
|
12
|
-
const midday = this.getStart() + Duration.Hours(12).ms;
|
|
13
|
-
|
|
14
|
-
return new Date(midday).toISOString().slice(0, 10) as DayIsoIdType;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
previous(): Day {
|
|
18
|
-
const shifted = this.getStart() - Duration.Days(1).ms;
|
|
19
|
-
|
|
20
|
-
return Day.fromTimestamp(Timestamp.parse(shifted));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
next(): Day {
|
|
24
|
-
const shifted = this.getStart() + Duration.Days(1).ms;
|
|
25
|
-
|
|
26
|
-
return Day.fromTimestamp(Timestamp.parse(shifted));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
shift(count: number): Day {
|
|
30
|
-
const shifted = this.getStart() + count * Duration.Days(1).ms;
|
|
31
|
-
|
|
32
|
-
return Day.fromTimestamp(Timestamp.parse(shifted));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
12
|
static fromTimestamp(timestamp: TimestampType): Day {
|
|
36
13
|
const date = new Date(timestamp);
|
|
14
|
+
|
|
37
15
|
const startUtc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
38
16
|
const endUtc = startUtc + Duration.Days(1).ms - 1;
|
|
39
17
|
|
|
@@ -46,9 +24,34 @@ export class Day extends DateRange {
|
|
|
46
24
|
|
|
47
25
|
static fromIsoId(isoId: DayIsoIdType): Day {
|
|
48
26
|
const [year, month, day] = DayIsoId.parse(isoId).split("-").map(Number);
|
|
27
|
+
|
|
49
28
|
const startUtc = Date.UTC(year, month - 1, day);
|
|
50
29
|
const endUtc = startUtc + Duration.Days(1).ms - 1;
|
|
51
30
|
|
|
52
31
|
return new Day(Timestamp.parse(startUtc), Timestamp.parse(endUtc));
|
|
53
32
|
}
|
|
33
|
+
|
|
34
|
+
toIsoId(): DayIsoIdType {
|
|
35
|
+
const midday = this.getStart() + Duration.Hours(12).ms;
|
|
36
|
+
|
|
37
|
+
return DayIsoId.parse(formatISO(midday, { representation: "date" }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
previous(): Day {
|
|
41
|
+
return this.shift(-1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
next(): Day {
|
|
45
|
+
return this.shift(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
shift(count: number): Day {
|
|
49
|
+
const timestamp = this.getStart() + count * Duration.Days(1).ms;
|
|
50
|
+
|
|
51
|
+
return Day.fromTimestamp(Timestamp.parse(timestamp));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toString(): string {
|
|
55
|
+
return this.toIsoId();
|
|
56
|
+
}
|
|
54
57
|
}
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export const DirectoryPathAbsoluteError = {
|
|
4
|
+
BadSegments: "directory.path.absolue.bad.segments",
|
|
5
|
+
Empty: "directory.path.absolue.empty",
|
|
6
|
+
LeadingSlash: "directory.path.absolue.leading.slash",
|
|
7
|
+
TooLong: "directory.path.absolue.too.long",
|
|
8
|
+
TrailingSlash: "directory.path.absolue.trailing.slash",
|
|
9
|
+
Type: "directory.path.absolue.type",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
// Letters, digits, dots, underscores, and hyphens
|
|
13
|
+
export const DIRECTORY_PATH_ABSOLUTE_CHARS = /^[a-zA-Z0-9._-]+$/;
|
|
14
|
+
|
|
15
|
+
const DOT_SEGMENTS = [".", ".."];
|
|
8
16
|
|
|
9
17
|
export const DirectoryPathAbsoluteSchema = z
|
|
10
|
-
.string(
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
.refine((value) =>
|
|
14
|
-
|
|
15
|
-
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), AbsDirControlCharsForbiddenError)
|
|
16
|
-
// collapse duplicate slashes, then drop trailing slash unless it's the root "/"
|
|
17
|
-
.transform((value) => value.replace(/\/{2,}/g, "/"))
|
|
18
|
-
.transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value))
|
|
18
|
+
.string(DirectoryPathAbsoluteError.Type)
|
|
19
|
+
.min(1, DirectoryPathAbsoluteError.Empty)
|
|
20
|
+
.max(512, DirectoryPathAbsoluteError.TooLong)
|
|
21
|
+
.refine((value) => value.startsWith("/"), DirectoryPathAbsoluteError.LeadingSlash)
|
|
22
|
+
.refine((value) => (value === "/" ? true : !value.endsWith("/")), DirectoryPathAbsoluteError.TrailingSlash)
|
|
19
23
|
.refine((value) => {
|
|
20
24
|
if (value === "/") return true;
|
|
25
|
+
|
|
21
26
|
const segments = value.slice(1).split("/");
|
|
27
|
+
|
|
22
28
|
return segments.every(
|
|
23
|
-
(segment) =>
|
|
24
|
-
segment.length > 0 && /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== "..",
|
|
29
|
+
(segment) => DIRECTORY_PATH_ABSOLUTE_CHARS.test(segment) && !DOT_SEGMENTS.includes(segment),
|
|
25
30
|
);
|
|
26
|
-
},
|
|
27
|
-
.brand("
|
|
31
|
+
}, DirectoryPathAbsoluteError.BadSegments)
|
|
32
|
+
.brand("DirectoryPathAbsoluteSchema");
|
|
28
33
|
|
|
29
34
|
export type DirectoryPathAbsoluteType = z.infer<typeof DirectoryPathAbsoluteSchema>;
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export const DirectoryPathRelativeError = {
|
|
4
|
+
BadSegments: "directory.path.relative.bad.segments",
|
|
5
|
+
Empty: "directory.path.relative.empty",
|
|
6
|
+
LeadingSlash: "directory.path.relative.leading.slash",
|
|
7
|
+
TooLong: "directory.path.absolue.too.long",
|
|
8
|
+
TrailingSlash: "directory.path.absolue.trailing.slash",
|
|
9
|
+
Type: "directory.path.relative.not.type",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
// Letters, digits, dots, underscores, and hyphens
|
|
13
|
+
export const DIRECTORY_PATH_RELATIVE_CHARS = /^[A-Za-z0-9._-]+$/;
|
|
14
|
+
|
|
15
|
+
const DOT_SEGMENTS = [".", ".."];
|
|
9
16
|
|
|
10
17
|
export const DirectoryPathRelativeSchema = z
|
|
11
|
-
.string(
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
.refine((value) => !value.
|
|
15
|
-
|
|
16
|
-
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), RelDirControlCharsForbiddenError)
|
|
17
|
-
.transform((value) => value.replace(/\/{2,}/g, "/"))
|
|
18
|
-
.transform((value) => value.replace(/^\/+|\/+$/g, ""))
|
|
19
|
-
.refine((value) => value.length > 0, RelDirEmptyError)
|
|
18
|
+
.string(DirectoryPathRelativeError.Type)
|
|
19
|
+
.min(1, DirectoryPathRelativeError.Empty)
|
|
20
|
+
.max(512, DirectoryPathRelativeError.TooLong)
|
|
21
|
+
.refine((value) => !value.startsWith("/"), DirectoryPathRelativeError.LeadingSlash)
|
|
22
|
+
.refine((value) => !value.endsWith("/"), DirectoryPathRelativeError.TrailingSlash)
|
|
20
23
|
.refine(
|
|
21
24
|
(value) =>
|
|
22
25
|
value
|
|
23
26
|
.split("/")
|
|
24
|
-
.every((segment) =>
|
|
25
|
-
|
|
27
|
+
.every((segment) => DIRECTORY_PATH_RELATIVE_CHARS.test(segment) && !DOT_SEGMENTS.includes(segment)),
|
|
28
|
+
DirectoryPathRelativeError.BadSegments,
|
|
26
29
|
)
|
|
27
|
-
.brand("
|
|
30
|
+
.brand("DirectoryPathRelativeSchema");
|
|
28
31
|
|
|
29
32
|
export type DirectoryPathRelativeType = z.infer<typeof DirectoryPathRelativeSchema>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export const DivisionFactorError = {
|
|
4
|
+
Type: "division.factor.type",
|
|
5
|
+
Invalid: "division.factor.invalid",
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export const DivisionFactor = z
|
|
9
|
+
.number(DivisionFactorError.Type)
|
|
10
|
+
.gt(0, DivisionFactorError.Invalid)
|
|
11
|
+
.brand("DivisionFactor");
|
|
12
|
+
|
|
13
|
+
export type DivisionFactorType = z.infer<typeof DivisionFactor>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export const DurationMsError = { Invalid: "duration.invalid" } as const;
|
|
4
|
+
|
|
5
|
+
export const DurationMs = z.number(DurationMsError.Invalid).int(DurationMsError.Invalid).brand("DurationMs");
|
|
6
|
+
|
|
7
|
+
export type DurationMsType = z.infer<typeof DurationMs>;
|
package/src/duration.service.ts
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DurationMs, type DurationMsType } from "./duration-ms.vo";
|
|
2
2
|
import { RoundToDecimal } from "./rounding.adapter";
|
|
3
3
|
import type { RoundingPort } from "./rounding.port";
|
|
4
|
-
import { Timestamp, type TimestampType } from "./timestamp.vo";
|
|
5
|
-
|
|
6
|
-
export const DurationMsError = { error: "duration.invalid" } as const;
|
|
7
|
-
|
|
8
|
-
export const DurationMsSchema = z
|
|
9
|
-
.number(DurationMsError)
|
|
10
|
-
.int(DurationMsError)
|
|
11
|
-
.refine(Number.isFinite, DurationMsError)
|
|
12
|
-
.brand("DurationMs");
|
|
13
|
-
|
|
14
|
-
export type DurationMsType = z.infer<typeof DurationMsSchema>;
|
|
15
4
|
|
|
16
5
|
export class Duration {
|
|
17
6
|
private static readonly rounding: RoundingPort = new RoundToDecimal(2);
|
|
18
|
-
private readonly
|
|
7
|
+
private readonly internal: DurationMsType;
|
|
19
8
|
|
|
20
9
|
private static readonly MS_IN_SECOND = 1_000;
|
|
21
|
-
private static readonly MS_IN_MINUTE =
|
|
22
|
-
private static readonly MS_IN_HOUR =
|
|
23
|
-
private static readonly MS_IN_DAY =
|
|
10
|
+
private static readonly MS_IN_MINUTE = 60 * Duration.MS_IN_SECOND;
|
|
11
|
+
private static readonly MS_IN_HOUR = 60 * Duration.MS_IN_MINUTE;
|
|
12
|
+
private static readonly MS_IN_DAY = 24 * Duration.MS_IN_HOUR;
|
|
24
13
|
|
|
25
14
|
private constructor(candidateMs: number) {
|
|
26
|
-
this.
|
|
15
|
+
this.internal = DurationMs.parse(candidateMs);
|
|
27
16
|
}
|
|
28
17
|
|
|
29
18
|
static Days(value: number): Duration {
|
|
@@ -43,48 +32,35 @@ export class Duration {
|
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
get days(): number {
|
|
46
|
-
return Duration.rounding.round(this.
|
|
35
|
+
return Duration.rounding.round(this.internal / Duration.MS_IN_DAY);
|
|
47
36
|
}
|
|
48
37
|
get hours(): number {
|
|
49
|
-
return Duration.rounding.round(this.
|
|
38
|
+
return Duration.rounding.round(this.internal / Duration.MS_IN_HOUR);
|
|
50
39
|
}
|
|
51
40
|
get minutes(): number {
|
|
52
|
-
return Duration.rounding.round(this.
|
|
41
|
+
return Duration.rounding.round(this.internal / Duration.MS_IN_MINUTE);
|
|
53
42
|
}
|
|
54
43
|
get seconds(): number {
|
|
55
|
-
return Duration.rounding.round(this.
|
|
44
|
+
return Duration.rounding.round(this.internal / Duration.MS_IN_SECOND);
|
|
56
45
|
}
|
|
57
46
|
get ms(): DurationMsType {
|
|
58
|
-
return this.
|
|
47
|
+
return this.internal;
|
|
59
48
|
}
|
|
60
49
|
|
|
61
50
|
isLongerThan(another: Duration): boolean {
|
|
62
|
-
return this.
|
|
51
|
+
return this.internal > another.internal;
|
|
63
52
|
}
|
|
64
53
|
isShorterThan(another: Duration): boolean {
|
|
65
|
-
return this.
|
|
54
|
+
return this.internal < another.internal;
|
|
66
55
|
}
|
|
67
56
|
|
|
68
57
|
equals(other: Duration): boolean {
|
|
69
|
-
return this.
|
|
58
|
+
return this.internal === other.internal;
|
|
70
59
|
}
|
|
71
60
|
add(another: Duration): Duration {
|
|
72
|
-
return Duration.Ms(this.
|
|
61
|
+
return Duration.Ms(this.internal + another.internal);
|
|
73
62
|
}
|
|
74
63
|
subtract(another: Duration): Duration {
|
|
75
|
-
return Duration.Ms(this.
|
|
64
|
+
return Duration.Ms(this.internal - another.internal);
|
|
76
65
|
}
|
|
77
66
|
}
|
|
78
|
-
|
|
79
|
-
export const Time = {
|
|
80
|
-
Now(now: TimestampType) {
|
|
81
|
-
return {
|
|
82
|
-
Add(duration: Duration): TimestampType {
|
|
83
|
-
return Timestamp.parse(now + duration.ms);
|
|
84
|
-
},
|
|
85
|
-
Minus(duration: Duration): TimestampType {
|
|
86
|
-
return Timestamp.parse(now - duration.ms);
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
};
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import { z } from "zod/v4";
|
|
2
|
-
|
|
3
|
-
export const Email = z.email().brand("Email");
|
|
4
|
-
export type EmailType = z.infer<typeof Email>;
|
|
5
|
-
|
|
6
|
-
type EmailMaskedType = string;
|
|
7
|
-
|
|
8
1
|
export class EmailMask {
|
|
9
|
-
static censor(email:
|
|
10
|
-
const [
|
|
2
|
+
static censor(email: string): string {
|
|
3
|
+
const [local, domain] = email.split("@");
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
const domain = afterAt as string;
|
|
5
|
+
if (local.length <= 2) return `${"*".repeat(local.length)}@${domain}`;
|
|
14
6
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const firstCharacter = local.at(0);
|
|
8
|
+
const censoredPart = "*".repeat(local.length - 2);
|
|
9
|
+
const lastCharacter = local.at(-1);
|
|
18
10
|
|
|
19
|
-
const censoredLocal = `${
|
|
11
|
+
const censoredLocal = `${firstCharacter}${censoredPart}${lastCharacter}`;
|
|
20
12
|
|
|
21
13
|
return `${censoredLocal}@${domain}`;
|
|
22
14
|
}
|