@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
|
@@ -2,14 +2,21 @@ import { z } from "zod/v4";
|
|
|
2
2
|
import { DirectoryPathAbsoluteSchema } from "./directory-path-absolute.vo";
|
|
3
3
|
import { Filename } from "./filename.vo";
|
|
4
4
|
|
|
5
|
+
export const AbsFilePathTypeError = "abs.file.path.not.string" as const;
|
|
6
|
+
export const AbsFilePathMustStartWithSlashError = "abs_file_path_must_start_with_slash" as const;
|
|
7
|
+
export const AbsFilePathBackslashForbiddenError = "abs_file_path_backslash_forbidden" as const;
|
|
8
|
+
export const AbsFilePathMissingFilenameError = "abs_file_path_missing_filename" as const;
|
|
9
|
+
|
|
5
10
|
export const FilePathAbsoluteSchema = z
|
|
6
|
-
.string()
|
|
11
|
+
.string(AbsFilePathTypeError)
|
|
7
12
|
.trim()
|
|
8
|
-
.refine((value) => value.startsWith("/"),
|
|
9
|
-
.refine((value) => !value.includes("\\"),
|
|
10
|
-
|
|
11
|
-
.transform((value) =>
|
|
12
|
-
|
|
13
|
+
.refine((value) => value.startsWith("/"), AbsFilePathMustStartWithSlashError)
|
|
14
|
+
.refine((value) => !value.includes("\\"), AbsFilePathBackslashForbiddenError)
|
|
15
|
+
// collapse duplicate slashes
|
|
16
|
+
.transform((value) => value.replace(/\/{2,}/g, "/"))
|
|
17
|
+
// keep "/" as-is; otherwise remove a trailing slash
|
|
18
|
+
.transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value))
|
|
19
|
+
.refine((value) => value !== "/", AbsFilePathMissingFilenameError)
|
|
13
20
|
.transform((normalized) => {
|
|
14
21
|
const lastSlashIndex = normalized.lastIndexOf("/");
|
|
15
22
|
const directoryCandidate = lastSlashIndex === 0 ? "/" : normalized.slice(0, lastSlashIndex);
|
|
@@ -20,3 +27,5 @@ export const FilePathAbsoluteSchema = z
|
|
|
20
27
|
|
|
21
28
|
return { directory, filename };
|
|
22
29
|
});
|
|
30
|
+
|
|
31
|
+
export type FilePathAbsoluteType = z.infer<typeof FilePathAbsoluteSchema>;
|
|
@@ -2,14 +2,19 @@ import { z } from "zod/v4";
|
|
|
2
2
|
import { DirectoryPathRelativeSchema } from "./directory-path-relative.vo";
|
|
3
3
|
import { Filename } from "./filename.vo";
|
|
4
4
|
|
|
5
|
+
export const RelFilePathTypeError = "rel.file.path.not.string" as const;
|
|
6
|
+
export const RelFilePathMustNotStartWithSlashError = "rel_file_path_must_not_start_with_slash" as const;
|
|
7
|
+
export const RelFilePathBackslashForbiddenError = "rel_file_path_backslash_forbidden" as const;
|
|
8
|
+
export const RelFilePathRequiresDirectoryError = "rel_file_path_requires_directory" as const;
|
|
9
|
+
|
|
5
10
|
export const FilePathRelativeSchema = z
|
|
6
|
-
.string()
|
|
11
|
+
.string(RelFilePathTypeError)
|
|
7
12
|
.trim()
|
|
8
|
-
.refine((value) => !value.startsWith("/"),
|
|
9
|
-
.refine((value) => !value.includes("\\"),
|
|
10
|
-
|
|
11
|
-
.transform((value) => value.replace(/^\/+|\/+$/g, ""))
|
|
12
|
-
.refine((value) => value.includes("/"),
|
|
13
|
+
.refine((value) => !value.startsWith("/"), RelFilePathMustNotStartWithSlashError)
|
|
14
|
+
.refine((value) => !value.includes("\\"), RelFilePathBackslashForbiddenError)
|
|
15
|
+
// collapse duplicate slashes, then trim leading/trailing slashes
|
|
16
|
+
.transform((value) => value.replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, ""))
|
|
17
|
+
.refine((value) => value.includes("/"), RelFilePathRequiresDirectoryError)
|
|
13
18
|
.transform((normalized) => {
|
|
14
19
|
const lastSlashIndex = normalized.lastIndexOf("/");
|
|
15
20
|
const directoryCandidate = normalized.slice(0, lastSlashIndex);
|
|
@@ -20,3 +25,5 @@ export const FilePathRelativeSchema = z
|
|
|
20
25
|
|
|
21
26
|
return { directory, filename };
|
|
22
27
|
});
|
|
28
|
+
|
|
29
|
+
export type FilePathRelativeType = z.infer<typeof FilePathRelativeSchema>;
|
package/src/file-path.vo.ts
CHANGED
|
@@ -10,21 +10,23 @@ export class FilePathRelative {
|
|
|
10
10
|
private readonly filename: Filename,
|
|
11
11
|
) {}
|
|
12
12
|
|
|
13
|
-
static fromParts(directoryCandidate: string, filename: Filename) {
|
|
13
|
+
static fromParts(directoryCandidate: string, filename: Filename): FilePathRelative {
|
|
14
14
|
const directory = DirectoryPathRelativeSchema.parse(directoryCandidate);
|
|
15
|
+
|
|
15
16
|
return new FilePathRelative(directory, filename);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
static fromPartsSafe(directory: DirectoryPathRelativeType, filename: Filename) {
|
|
19
|
+
static fromPartsSafe(directory: DirectoryPathRelativeType, filename: Filename): FilePathRelative {
|
|
19
20
|
return new FilePathRelative(directory, filename);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
static fromString(pathCandidate: string): FilePathRelative {
|
|
23
|
-
const
|
|
24
|
-
|
|
24
|
+
const parsed = FilePathRelativeSchema.parse(pathCandidate);
|
|
25
|
+
|
|
26
|
+
return new FilePathRelative(parsed.directory, parsed.filename);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
get() {
|
|
29
|
+
get(): string {
|
|
28
30
|
return `${this.directory}/${this.filename.get()}`;
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -55,22 +57,24 @@ export class FilePathAbsolute {
|
|
|
55
57
|
private readonly filename: Filename,
|
|
56
58
|
) {}
|
|
57
59
|
|
|
58
|
-
static fromParts(directoryCandidate: string, filename: Filename) {
|
|
60
|
+
static fromParts(directoryCandidate: string, filename: Filename): FilePathAbsolute {
|
|
59
61
|
const directory = DirectoryPathAbsoluteSchema.parse(directoryCandidate);
|
|
62
|
+
|
|
60
63
|
return new FilePathAbsolute(directory, filename);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
static fromPartsSafe(directory: DirectoryPathAbsoluteType, filename: Filename) {
|
|
66
|
+
static fromPartsSafe(directory: DirectoryPathAbsoluteType, filename: Filename): FilePathAbsolute {
|
|
64
67
|
return new FilePathAbsolute(directory, filename);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
static fromString(pathCandidate: string): FilePathAbsolute {
|
|
68
|
-
const
|
|
69
|
-
|
|
71
|
+
const parsed = FilePathAbsoluteSchema.parse(pathCandidate);
|
|
72
|
+
|
|
73
|
+
return new FilePathAbsolute(parsed.directory, parsed.filename);
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
get() {
|
|
73
|
-
if (this.directory ===
|
|
76
|
+
get(): string {
|
|
77
|
+
if (this.directory === "/") return `/${this.filename.get()}`;
|
|
74
78
|
return `${this.directory}/${this.filename.get()}`;
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Basename } from "./basename.vo";
|
|
3
|
+
import { Extension } from "./extension.vo";
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
|
|
5
|
+
export const FilenameTypeError = "filename.not.string" as const;
|
|
6
|
+
export const FilenameInvalidError = "filename.invalid" as const;
|
|
7
|
+
|
|
8
|
+
export const FilenameFromString = z
|
|
9
|
+
.string(FilenameTypeError)
|
|
7
10
|
.trim()
|
|
8
|
-
.refine((
|
|
9
|
-
const index =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.transform((
|
|
14
|
-
const index =
|
|
15
|
-
const
|
|
16
|
-
const extension =
|
|
17
|
-
|
|
11
|
+
.refine((value) => {
|
|
12
|
+
const index = value.lastIndexOf(".");
|
|
13
|
+
|
|
14
|
+
return index > 0 && index < value.length - 1;
|
|
15
|
+
}, FilenameInvalidError)
|
|
16
|
+
.transform((value) => {
|
|
17
|
+
const index = value.lastIndexOf(".");
|
|
18
|
+
const basename = Basename.parse(value.slice(0, index));
|
|
19
|
+
const extension = Extension.parse(value.slice(index + 1));
|
|
20
|
+
|
|
21
|
+
return { basename, extension };
|
|
18
22
|
});
|
|
19
|
-
|
|
23
|
+
|
|
24
|
+
export type FilenameFromStringType = z.infer<typeof FilenameFromString>;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
3
|
+
export const FilenameSuffixTypeError = "suffix.not.string" as const;
|
|
4
|
+
export const FilenameSuffixTooLongError = "suffix_too_long" as const;
|
|
5
|
+
|
|
6
|
+
export const FilenameSuffix = z
|
|
7
|
+
.string(FilenameSuffixTypeError)
|
|
5
8
|
.trim()
|
|
6
9
|
.transform((value) => value.replace(/[^A-Za-z0-9_-]/g, ""))
|
|
7
|
-
.
|
|
10
|
+
.refine((value) => value.length <= 32, FilenameSuffixTooLongError)
|
|
8
11
|
.brand("basename_suffix");
|
|
9
|
-
|
|
12
|
+
|
|
13
|
+
export type FilenameSuffixType = z.infer<typeof FilenameSuffix>;
|
package/src/filename.vo.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { Basename, type BasenameType } from "./basename.vo";
|
|
2
|
+
import { Extension, type ExtensionType } from "./extension.vo";
|
|
3
|
+
import { FilenameFromString } from "./filename-from-string.vo";
|
|
4
|
+
import { FilenameSuffix, type FilenameSuffixType } from "./filename-suffix.vo";
|
|
5
5
|
|
|
6
6
|
export class Filename {
|
|
7
7
|
private constructor(
|
|
@@ -9,21 +9,20 @@ export class Filename {
|
|
|
9
9
|
private readonly extension: ExtensionType,
|
|
10
10
|
) {}
|
|
11
11
|
|
|
12
|
-
static fromParts(basename: string, extension: string) {
|
|
13
|
-
return new Filename(
|
|
12
|
+
static fromParts(basename: string, extension: string): Filename {
|
|
13
|
+
return new Filename(Basename.parse(basename), Extension.parse(extension));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
static fromPartsSafe(basename: BasenameType, extension: ExtensionType) {
|
|
16
|
+
static fromPartsSafe(basename: BasenameType, extension: ExtensionType): Filename {
|
|
17
17
|
return new Filename(basename, extension);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
static fromString(candidate: string) {
|
|
21
|
-
const { basename, extension } =
|
|
22
|
-
|
|
20
|
+
static fromString(candidate: string): Filename {
|
|
21
|
+
const { basename, extension } = FilenameFromString.parse(candidate);
|
|
23
22
|
return new Filename(basename, extension);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
get() {
|
|
25
|
+
get(): string {
|
|
27
26
|
return `${this.basename}.${this.extension}`;
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -44,14 +43,14 @@ export class Filename {
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
withSuffix(candidate: string): Filename {
|
|
47
|
-
const suffix =
|
|
48
|
-
const basename =
|
|
46
|
+
const suffix = FilenameSuffix.parse(candidate);
|
|
47
|
+
const basename = Basename.parse(`${this.basename}${suffix}`);
|
|
49
48
|
|
|
50
49
|
return new Filename(basename, this.extension);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
withSuffixSafe(suffix:
|
|
54
|
-
const basename =
|
|
52
|
+
withSuffixSafe(suffix: FilenameSuffixType): Filename {
|
|
53
|
+
const basename = Basename.parse(`${this.basename}${suffix}`);
|
|
55
54
|
|
|
56
55
|
return new Filename(basename, this.extension);
|
|
57
56
|
}
|
package/src/height.vo.ts
CHANGED
|
@@ -2,14 +2,24 @@ import { z } from "zod/v4";
|
|
|
2
2
|
import { RoundToDecimal, RoundToNearest } from "./rounding.adapter";
|
|
3
3
|
import type { RoundingPort } from "./rounding.port";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const NonFiniteNumberError = { error: "number.non_finite" } as const;
|
|
6
|
+
const NumberNegativeError = { error: "number.negative" } as const;
|
|
7
|
+
const MillimetersIntegerNonNegativeError = { error: "millimeters.integer_non_negative" } as const;
|
|
8
|
+
const IntegerNonNegativeError = { error: "integer.non_negative" } as const;
|
|
9
|
+
|
|
10
|
+
const HeightFiniteNumber = z.number(NonFiniteNumberError).refine(Number.isFinite, NonFiniteNumberError);
|
|
11
|
+
|
|
12
|
+
const HeightNonNegativeQuantity = HeightFiniteNumber.min(0, NumberNegativeError);
|
|
13
|
+
|
|
14
|
+
const HeightCanonicalMillimeters = HeightFiniteNumber.int(MillimetersIntegerNonNegativeError).min(
|
|
15
|
+
0,
|
|
16
|
+
MillimetersIntegerNonNegativeError,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const HeightRoundedWholeInches = HeightFiniteNumber.int(IntegerNonNegativeError).min(
|
|
20
|
+
0,
|
|
21
|
+
IntegerNonNegativeError,
|
|
22
|
+
);
|
|
13
23
|
|
|
14
24
|
export enum HeightUnit {
|
|
15
25
|
cm = "cm",
|
|
@@ -24,88 +34,96 @@ export class Height {
|
|
|
24
34
|
private constructor(private readonly millimeters: number) {}
|
|
25
35
|
|
|
26
36
|
static fromCentimeters(centimeters: number, rounding: RoundingPort = new RoundToNearest()): Height {
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
const validatedCentimeters = HeightNonNegativeQuantity.parse(centimeters);
|
|
38
|
+
const millimetersFloat = validatedCentimeters * Height.MILLIMETERS_PER_CENTIMETER;
|
|
39
|
+
const millimetersRounded = rounding.round(millimetersFloat);
|
|
40
|
+
const validatedMillimeters = HeightCanonicalMillimeters.parse(millimetersRounded);
|
|
41
|
+
|
|
42
|
+
return new Height(validatedMillimeters);
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
static fromFeetInches(feet: number, inches = 0, rounding: RoundingPort = new RoundToNearest()): Height {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const totalInches =
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const validatedFeet = HeightNonNegativeQuantity.parse(feet);
|
|
47
|
+
const validatedInches = HeightNonNegativeQuantity.parse(inches);
|
|
48
|
+
const totalInches = validatedFeet * Height.INCHES_PER_FOOT + validatedInches;
|
|
49
|
+
const millimetersFloat = totalInches * Height.MILLIMETERS_PER_INCH;
|
|
50
|
+
const millimetersRounded = rounding.round(millimetersFloat);
|
|
51
|
+
const validatedMillimeters = HeightCanonicalMillimeters.parse(millimetersRounded);
|
|
52
|
+
|
|
53
|
+
return new Height(validatedMillimeters);
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
static fromMillimeters(millimeters: number, rounding: RoundingPort = new RoundToNearest()): Height {
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
const validatedMillimetersInput = HeightNonNegativeQuantity.parse(millimeters);
|
|
58
|
+
const millimetersRounded = rounding.round(validatedMillimetersInput);
|
|
59
|
+
const validatedMillimeters = HeightCanonicalMillimeters.parse(millimetersRounded);
|
|
60
|
+
|
|
61
|
+
return new Height(validatedMillimeters);
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
static zero(): Height {
|
|
52
65
|
return new Height(0);
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
get(): number {
|
|
69
|
+
return this.millimeters;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
toMillimeters(): number {
|
|
56
73
|
return this.millimeters;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
toCentimeters(rounding?: RoundingPort): number {
|
|
60
|
-
const
|
|
61
|
-
|
|
77
|
+
const centimeters = this.millimeters / Height.MILLIMETERS_PER_CENTIMETER;
|
|
78
|
+
|
|
79
|
+
if (rounding) return rounding.round(centimeters);
|
|
80
|
+
return centimeters;
|
|
62
81
|
}
|
|
63
82
|
|
|
64
|
-
toFeetInches(rounding: RoundingPort = new RoundToNearest()): {
|
|
65
|
-
feet: number;
|
|
66
|
-
inches: number;
|
|
67
|
-
} {
|
|
83
|
+
toFeetInches(rounding: RoundingPort = new RoundToNearest()): { feet: number; inches: number } {
|
|
68
84
|
const totalInchesFloat = this.millimeters / Height.MILLIMETERS_PER_INCH;
|
|
69
85
|
const totalInchesRounded = rounding.round(totalInchesFloat);
|
|
70
|
-
const
|
|
86
|
+
const totalWholeInches = HeightRoundedWholeInches.parse(totalInchesRounded);
|
|
71
87
|
|
|
72
|
-
const feet = (
|
|
73
|
-
const inches =
|
|
88
|
+
const feet = Math.floor(totalWholeInches / Height.INCHES_PER_FOOT);
|
|
89
|
+
const inches = totalWholeInches % Height.INCHES_PER_FOOT;
|
|
74
90
|
|
|
75
91
|
return { feet, inches };
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
format(unit: HeightUnit, rounding?: RoundingPort): string {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
if (unit === HeightUnit.cm) {
|
|
96
|
+
const chosen = rounding ?? new RoundToDecimal(1);
|
|
97
|
+
const value = this.toCentimeters(chosen);
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
return `${value} cm`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const chosen = rounding ?? new RoundToNearest();
|
|
103
|
+
const parts = this.toFeetInches(chosen);
|
|
104
|
+
|
|
105
|
+
return `${parts.feet}′${parts.inches}″`;
|
|
106
|
+
}
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}[unit]();
|
|
108
|
+
toString(): string {
|
|
109
|
+
return this.format(HeightUnit.cm, new RoundToDecimal(1));
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
equals(
|
|
95
|
-
return this.millimeters ===
|
|
112
|
+
equals(another: Height): boolean {
|
|
113
|
+
return this.millimeters === another.millimeters;
|
|
96
114
|
}
|
|
97
115
|
|
|
98
|
-
compare(
|
|
99
|
-
if (this.equals(
|
|
100
|
-
return this.millimeters <
|
|
116
|
+
compare(another: Height): -1 | 0 | 1 {
|
|
117
|
+
if (this.equals(another)) return 0;
|
|
118
|
+
return this.millimeters < another.millimeters ? -1 : 1;
|
|
101
119
|
}
|
|
102
120
|
|
|
103
|
-
greaterThan(
|
|
104
|
-
return this.millimeters >
|
|
121
|
+
greaterThan(another: Height): boolean {
|
|
122
|
+
return this.millimeters > another.millimeters;
|
|
105
123
|
}
|
|
106
124
|
|
|
107
|
-
lessThan(
|
|
108
|
-
return this.millimeters <
|
|
125
|
+
lessThan(another: Height): boolean {
|
|
126
|
+
return this.millimeters < another.millimeters;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
isZero(): boolean {
|
package/src/index.ts
CHANGED
|
@@ -53,7 +53,6 @@ export * from "./rounding.adapter";
|
|
|
53
53
|
export * from "./rounding.port";
|
|
54
54
|
export * from "./simple-linear-regression.service";
|
|
55
55
|
export * from "./size.vo";
|
|
56
|
-
export * from "./stepper.service";
|
|
57
56
|
export * from "./stopwatch.service";
|
|
58
57
|
export * from "./streak-calculator.service";
|
|
59
58
|
export * from "./sum.service";
|
package/src/mime.vo.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as mime from "mime-types";
|
|
2
|
-
import {
|
|
2
|
+
import { Extension, type ExtensionType } from "./extension.vo";
|
|
3
3
|
|
|
4
4
|
export type MimeRawType = string;
|
|
5
5
|
type MimeTypeType = string;
|
|
6
6
|
type MimeSubtypeType = string;
|
|
7
7
|
|
|
8
|
+
export const InvalidMimeErrorMessage = "invalid.mime" as const;
|
|
9
|
+
export const NotAcceptedMimeErrorMessage = "mime.not.accepted" as const;
|
|
10
|
+
|
|
8
11
|
export class Mime {
|
|
9
12
|
readonly raw: MimeRawType;
|
|
10
13
|
readonly type: MimeTypeType;
|
|
@@ -29,29 +32,29 @@ export class Mime {
|
|
|
29
32
|
if (this.raw === another.raw) return true;
|
|
30
33
|
|
|
31
34
|
const typeMatches = this.type === another.type || this.type === "*";
|
|
32
|
-
if (!typeMatches) return false;
|
|
33
35
|
|
|
36
|
+
if (!typeMatches) return false;
|
|
34
37
|
return this.subtype === another.subtype || this.subtype === "*";
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
toExtension(): ExtensionType {
|
|
38
|
-
return
|
|
41
|
+
return Extension.parse(mime.extension(this.raw));
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export class InvalidMimeError extends Error {
|
|
43
46
|
constructor() {
|
|
44
|
-
super();
|
|
47
|
+
super(InvalidMimeErrorMessage);
|
|
45
48
|
Object.setPrototypeOf(this, InvalidMimeError.prototype);
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export class NotAcceptedMimeError extends Error {
|
|
50
53
|
mime: MimeRawType;
|
|
51
|
-
constructor(
|
|
52
|
-
super();
|
|
54
|
+
constructor(mimeValue: MimeRawType) {
|
|
55
|
+
super(NotAcceptedMimeErrorMessage);
|
|
53
56
|
Object.setPrototypeOf(this, NotAcceptedMimeError.prototype);
|
|
54
|
-
this.mime =
|
|
57
|
+
this.mime = mimeValue;
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
package/src/month-iso-id.vo.ts
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
.string()
|
|
5
|
-
.regex(/^\d{4}-\d{2}$/)
|
|
6
|
-
.refine(
|
|
7
|
-
(value) => {
|
|
8
|
-
const [y, m] = value.split("-");
|
|
9
|
-
|
|
10
|
-
const year = Number(y);
|
|
11
|
-
const month = Number(m);
|
|
3
|
+
export const MonthIsoIdError = { error: "month-iso-id.invalid" } as const;
|
|
12
4
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ message: "month-iso-id.invalid" },
|
|
23
|
-
);
|
|
5
|
+
export const MonthIsoId = z
|
|
6
|
+
.string(MonthIsoIdError)
|
|
7
|
+
.regex(/^\d{4}-\d{2}$/, MonthIsoIdError)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
const year = Number(value.slice(0, 4));
|
|
10
|
+
const month = Number(value.slice(5, 7));
|
|
11
|
+
|
|
12
|
+
return Number.isInteger(year) && Number.isInteger(month) && month >= 1 && month <= 12;
|
|
13
|
+
}, MonthIsoIdError);
|
|
24
14
|
|
|
25
15
|
export type MonthIsoIdType = z.infer<typeof MonthIsoId>;
|
package/src/month.vo.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/month.vo.ts
|
|
2
2
|
import { DateRange } from "./date-range.vo";
|
|
3
3
|
import { MonthIsoId, type MonthIsoIdType } from "./month-iso-id.vo";
|
|
4
4
|
import { Timestamp, type TimestampType } from "./timestamp.vo";
|
|
@@ -9,24 +9,24 @@ export class Month extends DateRange {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
previous(): Month {
|
|
12
|
-
|
|
13
|
-
return Month.fromTimestamp(Timestamp.parse(shifted));
|
|
12
|
+
return Month.fromTimestamp(Timestamp.parse(this.getStart() - 1));
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
next(): Month {
|
|
17
|
-
|
|
18
|
-
return Month.fromTimestamp(Timestamp.parse(shifted));
|
|
16
|
+
return Month.fromTimestamp(Timestamp.parse(this.getEnd() + 1));
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
shift(count: number): Month {
|
|
22
|
-
const
|
|
23
|
-
|
|
20
|
+
const date = new Date(this.getStart());
|
|
21
|
+
date.setUTCMonth(date.getUTCMonth() + count);
|
|
22
|
+
|
|
23
|
+
return Month.fromTimestamp(Timestamp.parse(date.getTime()));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
static fromTimestamp(timestamp: TimestampType): Month {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
return
|
|
27
|
+
const isoMonth = new Date(timestamp).toISOString().slice(0, 7) as MonthIsoIdType;
|
|
28
|
+
|
|
29
|
+
return Month.fromIsoId(isoMonth);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
static fromNow(now: TimestampType): Month {
|
|
@@ -34,8 +34,14 @@ export class Month extends DateRange {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
static fromIsoId(iso: MonthIsoIdType): Month {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
37
|
+
const validated = MonthIsoId.parse(iso);
|
|
38
|
+
const year = Number(validated.slice(0, 4));
|
|
39
|
+
const monthIndex = Number(validated.slice(5, 7)) - 1;
|
|
40
|
+
|
|
41
|
+
const startUtc = Date.UTC(year, monthIndex, 1);
|
|
42
|
+
const nextStartUtc = Date.UTC(year, monthIndex + 1, 1);
|
|
43
|
+
const endUtc = nextStartUtc - 1;
|
|
44
|
+
|
|
45
|
+
return new Month(Timestamp.parse(startUtc), Timestamp.parse(endUtc));
|
|
40
46
|
}
|
|
41
47
|
}
|
package/src/object-key.vo.ts
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
+
export const ObjectKeyMustNotStartWithSlashError = "obj_key_must_not_start_with_slash" as const;
|
|
4
|
+
export const ObjectKeyBackslashForbiddenError = "obj_key_backslash_forbidden" as const;
|
|
5
|
+
export const ObjectKeyControlCharsForbiddenError = "obj_key_control_chars_forbidden" as const;
|
|
6
|
+
export const ObjectKeyEmptyError = "obj_key_empty" as const;
|
|
7
|
+
export const ObjectKeyBadSegmentsError = "obj_key_bad_segments" as const;
|
|
8
|
+
|
|
9
|
+
// biome-ignore lint: lint/suspicious/noControlCharactersInRegex
|
|
10
|
+
const CONTROL_CHARS_REGEX = /[\u0000-\u001F\u007F]/;
|
|
11
|
+
const SEGMENT_ALLOWED_REGEX = /^[a-z0-9._-]+$/;
|
|
12
|
+
|
|
3
13
|
export const ObjectKey = z
|
|
4
14
|
.string()
|
|
5
15
|
.trim()
|
|
6
|
-
|
|
7
|
-
.refine((
|
|
8
|
-
|
|
9
|
-
.refine((
|
|
10
|
-
.refine((
|
|
16
|
+
// fastest early exits first:
|
|
17
|
+
.refine((value) => value.length > 0, ObjectKeyEmptyError)
|
|
18
|
+
.refine((value) => !value.startsWith("/"), ObjectKeyMustNotStartWithSlashError)
|
|
19
|
+
.refine((value) => !value.includes("\\"), ObjectKeyBackslashForbiddenError)
|
|
20
|
+
.refine((value) => !CONTROL_CHARS_REGEX.test(value), ObjectKeyControlCharsForbiddenError)
|
|
11
21
|
.refine(
|
|
12
|
-
(
|
|
13
|
-
|
|
22
|
+
(value) =>
|
|
23
|
+
value
|
|
24
|
+
.split("/")
|
|
25
|
+
.every((segment) => SEGMENT_ALLOWED_REGEX.test(segment) && segment !== "." && segment !== ".."),
|
|
26
|
+
ObjectKeyBadSegmentsError,
|
|
14
27
|
)
|
|
15
28
|
.brand("object_key");
|
|
29
|
+
|
|
16
30
|
export type ObjectKeyType = z.infer<typeof ObjectKey>;
|