@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.
Files changed (119) hide show
  1. package/dist/basename.vo.d.ts +11 -2
  2. package/dist/basename.vo.js +22 -13
  3. package/dist/date-calculator.service.d.ts +2 -2
  4. package/dist/date-calculator.service.js +10 -11
  5. package/dist/date-range.vo.d.ts +1 -0
  6. package/dist/date-range.vo.js +2 -1
  7. package/dist/day-iso-id.vo.d.ts +3 -0
  8. package/dist/day-iso-id.vo.js +4 -4
  9. package/dist/day.vo.js +12 -10
  10. package/dist/directory-path-absolute.vo.d.ts +5 -0
  11. package/dist/directory-path-absolute.vo.js +11 -5
  12. package/dist/directory-path-relative.vo.d.ts +6 -0
  13. package/dist/directory-path-relative.vo.js +12 -6
  14. package/dist/dll.service.js +37 -31
  15. package/dist/extension.vo.d.ts +6 -2
  16. package/dist/extension.vo.js +11 -9
  17. package/dist/file-path-absolute-schema.vo.d.ts +5 -0
  18. package/dist/file-path-absolute-schema.vo.js +12 -6
  19. package/dist/file-path-relative-schema.vo.d.ts +6 -1
  20. package/dist/file-path-relative-schema.vo.js +10 -6
  21. package/dist/file-path.vo.js +4 -4
  22. package/dist/filename-from-string.vo.d.ts +6 -4
  23. package/dist/filename-from-string.vo.js +15 -14
  24. package/dist/filename-suffix.vo.d.ts +4 -2
  25. package/dist/filename-suffix.vo.js +5 -3
  26. package/dist/filename.vo.d.ts +2 -2
  27. package/dist/filename.vo.js +9 -9
  28. package/dist/height.vo.d.ts +6 -4
  29. package/dist/height.vo.js +56 -51
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.js +0 -1
  32. package/dist/mime.vo.d.ts +3 -1
  33. package/dist/mime.vo.js +8 -6
  34. package/dist/month-iso-id.vo.d.ts +3 -0
  35. package/dist/month-iso-id.vo.js +7 -12
  36. package/dist/month.vo.js +15 -13
  37. package/dist/object-key.vo.d.ts +5 -0
  38. package/dist/object-key.vo.js +16 -6
  39. package/dist/package-version.vo.d.ts +3 -0
  40. package/dist/package-version.vo.js +12 -34
  41. package/dist/pagination.service.d.ts +1 -1
  42. package/dist/pagination.service.js +11 -11
  43. package/dist/quarter-iso-id.vo.d.ts +3 -0
  44. package/dist/quarter-iso-id.vo.js +8 -7
  45. package/dist/rate-limiter.service.d.ts +3 -2
  46. package/dist/rate-limiter.service.js +4 -2
  47. package/dist/reordering.service.d.ts +20 -2
  48. package/dist/reordering.service.js +49 -29
  49. package/dist/revision.vo.d.ts +8 -3
  50. package/dist/revision.vo.js +13 -6
  51. package/dist/rounding.adapter.js +1 -2
  52. package/dist/size.vo.d.ts +1 -0
  53. package/dist/size.vo.js +4 -7
  54. package/dist/streak-calculator.service.d.ts +3 -4
  55. package/dist/streak-calculator.service.js +11 -17
  56. package/dist/time-zone-offset-value.vo.d.ts +1 -1
  57. package/dist/time-zone-offset-value.vo.js +1 -7
  58. package/dist/time.service.d.ts +11 -6
  59. package/dist/time.service.js +31 -18
  60. package/dist/timezone.vo.js +1 -3
  61. package/dist/tsconfig.tsbuildinfo +1 -1
  62. package/dist/week-iso-id.vo.d.ts +3 -0
  63. package/dist/week-iso-id.vo.js +4 -4
  64. package/dist/week.vo.js +1 -1
  65. package/dist/weekday.vo.d.ts +7 -6
  66. package/dist/weekday.vo.js +20 -13
  67. package/dist/weight.vo.d.ts +12 -0
  68. package/dist/weight.vo.js +37 -27
  69. package/dist/year-iso-id.vo.d.ts +3 -0
  70. package/dist/year-iso-id.vo.js +4 -6
  71. package/dist/year.vo.d.ts +2 -0
  72. package/dist/year.vo.js +4 -2
  73. package/package.json +1 -1
  74. package/readme.md +0 -1
  75. package/src/basename.vo.ts +25 -14
  76. package/src/clock.vo.ts +1 -0
  77. package/src/date-calculator.service.ts +10 -15
  78. package/src/date-range.vo.ts +3 -1
  79. package/src/day-iso-id.vo.ts +9 -10
  80. package/src/day.vo.ts +17 -10
  81. package/src/directory-path-absolute.vo.ts +12 -5
  82. package/src/directory-path-relative.vo.ts +13 -6
  83. package/src/dll.service.ts +45 -43
  84. package/src/extension.vo.ts +14 -12
  85. package/src/file-path-absolute-schema.vo.ts +15 -6
  86. package/src/file-path-relative-schema.vo.ts +13 -6
  87. package/src/file-path.vo.ts +15 -11
  88. package/src/filename-from-string.vo.ts +20 -15
  89. package/src/filename-suffix.vo.ts +8 -4
  90. package/src/filename.vo.ts +14 -15
  91. package/src/height.vo.ts +71 -53
  92. package/src/index.ts +0 -1
  93. package/src/mime.vo.ts +10 -7
  94. package/src/month-iso-id.vo.ts +10 -20
  95. package/src/month.vo.ts +19 -13
  96. package/src/object-key.vo.ts +21 -7
  97. package/src/outlier-detector.service.ts +1 -0
  98. package/src/package-version.vo.ts +18 -47
  99. package/src/pagination.service.ts +15 -13
  100. package/src/quarter-iso-id.vo.ts +11 -13
  101. package/src/quarter.vo.ts +3 -0
  102. package/src/rate-limiter.service.ts +7 -7
  103. package/src/reordering.service.ts +52 -38
  104. package/src/revision.vo.ts +17 -8
  105. package/src/rounding.adapter.ts +1 -3
  106. package/src/size.vo.ts +6 -16
  107. package/src/streak-calculator.service.ts +12 -17
  108. package/src/time-zone-offset-value.vo.ts +2 -7
  109. package/src/time.service.ts +43 -45
  110. package/src/timezone.vo.ts +1 -3
  111. package/src/week-iso-id.vo.ts +13 -14
  112. package/src/week.vo.ts +4 -2
  113. package/src/weekday.vo.ts +27 -13
  114. package/src/weight.vo.ts +49 -30
  115. package/src/year-iso-id.vo.ts +6 -9
  116. package/src/year.vo.ts +12 -2
  117. package/dist/stepper.service.d.ts +0 -23
  118. package/dist/stepper.service.js +0 -33
  119. 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("/"), "abs_file_path_must_start_with_slash")
9
- .refine((value) => !value.includes("\\"), "abs_file_path_backslash_forbidden")
10
- .transform((value) => value.replace(/\/{2,}/g, "/")) // collapse //
11
- .transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value)) // keep "/" as-is
12
- .refine((value) => value !== "/", "abs_file_path_missing_filename")
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("/"), "rel_file_path_must_not_start_with_slash")
9
- .refine((value) => !value.includes("\\"), "rel_file_path_backslash_forbidden")
10
- .transform((value) => value.replace(/\/{2,}/g, "/")) // collapse //
11
- .transform((value) => value.replace(/^\/+|\/+$/g, "")) // trim leading/trailing slashes
12
- .refine((value) => value.includes("/"), "rel_file_path_requires_directory")
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>;
@@ -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 { directory, filename } = FilePathRelativeSchema.parse(pathCandidate);
24
- return new FilePathRelative(directory, filename);
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 { directory, filename } = FilePathAbsoluteSchema.parse(pathCandidate);
69
- return new FilePathAbsolute(directory, filename);
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 === ("/" as DirectoryPathAbsoluteType)) return `/${this.filename.get()}`;
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 { BasenameSchema } from "./basename.vo";
3
- import { ExtensionSchema } from "./extension.vo";
2
+ import { Basename } from "./basename.vo";
3
+ import { Extension } from "./extension.vo";
4
4
 
5
- export const FilenameFromStringSchema = z
6
- .string()
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((string) => {
9
- const index = string.lastIndexOf(".");
10
- return index > 0 && index < string.length - 1;
11
- }, "filename_invalid")
12
- // split and validate parts using existing schemas
13
- .transform((string) => {
14
- const index = string.lastIndexOf(".");
15
- const base = BasenameSchema.parse(string.slice(0, index));
16
- const extension = ExtensionSchema.parse(string.slice(index + 1));
17
- return { basename: base, extension: extension };
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
- export type FilenameFromString = z.infer<typeof FilenameFromStringSchema>;
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 FilenameSuffixSchema = z
4
- .string()
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
- .pipe(z.string().max(32, "suffix_too_long"))
10
+ .refine((value) => value.length <= 32, FilenameSuffixTooLongError)
8
11
  .brand("basename_suffix");
9
- export type FilenameSuffixSchemaType = z.infer<typeof FilenameSuffixSchema>;
12
+
13
+ export type FilenameSuffixType = z.infer<typeof FilenameSuffix>;
@@ -1,7 +1,7 @@
1
- import { BasenameSchema, type BasenameType } from "./basename.vo";
2
- import { ExtensionSchema, type ExtensionType } from "./extension.vo";
3
- import { FilenameFromStringSchema } from "./filename-from-string.vo";
4
- import { FilenameSuffixSchema, type FilenameSuffixSchemaType } from "./filename-suffix.vo";
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(BasenameSchema.parse(basename), ExtensionSchema.parse(extension));
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 } = FilenameFromStringSchema.parse(candidate);
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 = FilenameSuffixSchema.parse(candidate);
48
- const basename = BasenameSchema.parse(`${this.basename}${suffix}`);
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: FilenameSuffixSchemaType): Filename {
54
- const basename = BasenameSchema.parse(`${this.basename}${suffix}`);
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 FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
6
- const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
7
- const NonNegativeIntegerMillimeters = FiniteNumericValue.int().min(0, {
8
- message: "Millimeters must be an integer greater than or equal to 0",
9
- });
10
- const NonNegativeIntegerValue = FiniteNumericValue.int().min(0, {
11
- message: "Value must be an integer greater than or equal to 0",
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
- NonNegativeNumericValue.parse(centimeters);
28
- const mmFloat = centimeters * Height.MILLIMETERS_PER_CENTIMETER;
29
- const mmRounded = rounding.round(mmFloat);
30
- NonNegativeIntegerMillimeters.parse(mmRounded);
31
- return new Height(mmRounded);
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
- NonNegativeNumericValue.parse(feet);
36
- NonNegativeNumericValue.parse(inches);
37
- const totalInches = feet * Height.INCHES_PER_FOOT + inches;
38
- const mmFloat = totalInches * Height.MILLIMETERS_PER_INCH;
39
- const mmRounded = rounding.round(mmFloat);
40
- NonNegativeIntegerMillimeters.parse(mmRounded);
41
- return new Height(mmRounded);
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
- NonNegativeNumericValue.parse(millimeters);
46
- const mmRounded = rounding.round(millimeters);
47
- NonNegativeIntegerMillimeters.parse(mmRounded);
48
- return new Height(mmRounded);
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 cm = this.millimeters / Height.MILLIMETERS_PER_CENTIMETER;
61
- return rounding ? rounding.round(cm) : cm;
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 integerInches = NonNegativeIntegerValue.parse(totalInchesRounded);
86
+ const totalWholeInches = HeightRoundedWholeInches.parse(totalInchesRounded);
71
87
 
72
- const feet = (integerInches - (integerInches % Height.INCHES_PER_FOOT)) / Height.INCHES_PER_FOOT;
73
- const inches = integerInches % Height.INCHES_PER_FOOT;
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
- return {
80
- [HeightUnit.cm]: () => {
81
- const chosen = rounding ?? new RoundToDecimal(1);
95
+ if (unit === HeightUnit.cm) {
96
+ const chosen = rounding ?? new RoundToDecimal(1);
97
+ const value = this.toCentimeters(chosen);
82
98
 
83
- return `${this.toCentimeters(chosen)} cm`;
84
- },
85
- [HeightUnit.ft_in]: () => {
86
- const chosen = rounding ?? new RoundToNearest();
87
- const { feet, inches } = this.toFeetInches(chosen);
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
- return `${feet}′${inches}″`;
90
- },
91
- }[unit]();
108
+ toString(): string {
109
+ return this.format(HeightUnit.cm, new RoundToDecimal(1));
92
110
  }
93
111
 
94
- equals(other: Height): boolean {
95
- return this.millimeters === other.millimeters;
112
+ equals(another: Height): boolean {
113
+ return this.millimeters === another.millimeters;
96
114
  }
97
115
 
98
- compare(other: Height): -1 | 0 | 1 {
99
- if (this.equals(other)) return 0;
100
- return this.millimeters < other.millimeters ? -1 : 1;
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(other: Height): boolean {
104
- return this.millimeters > other.millimeters;
121
+ greaterThan(another: Height): boolean {
122
+ return this.millimeters > another.millimeters;
105
123
  }
106
124
 
107
- lessThan(other: Height): boolean {
108
- return this.millimeters < other.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 { ExtensionSchema, type ExtensionType } from "./extension.vo";
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 ExtensionSchema.parse(mime.extension(this.raw));
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(mime: MimeRawType) {
52
- super();
54
+ constructor(mimeValue: MimeRawType) {
55
+ super(NotAcceptedMimeErrorMessage);
53
56
  Object.setPrototypeOf(this, NotAcceptedMimeError.prototype);
54
- this.mime = mime;
57
+ this.mime = mimeValue;
55
58
  }
56
59
  }
57
60
 
@@ -1,25 +1,15 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const MonthIsoId = z
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
- return (
14
- y.length === 4 &&
15
- m.length === 2 &&
16
- Number.isInteger(year) &&
17
- Number.isInteger(month) &&
18
- month >= 1 &&
19
- month <= 12
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
- import { addMonths, endOfMonth, startOfMonth } from "date-fns";
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
- const shifted = addMonths(new Date(this.getStart()), -1).getTime();
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
- const shifted = addMonths(new Date(this.getStart()), 1).getTime();
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 shifted = addMonths(new Date(this.getStart()), count).getTime();
23
- return Month.fromTimestamp(Timestamp.parse(shifted));
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 start = Timestamp.parse(startOfMonth(timestamp).getTime());
28
- const end = Timestamp.parse(endOfMonth(timestamp).getTime());
29
- return new Month(start, end);
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 [year, month] = MonthIsoId.parse(iso).split("-").map(Number);
38
- const reference = new Date(Date.UTC(year, month - 1, 1));
39
- return Month.fromTimestamp(Timestamp.parse(reference.getTime()));
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
  }
@@ -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
- .refine((v) => !v.startsWith("/"), "obj_key_must_not_start_with_slash")
7
- .refine((v) => !v.includes("\\"), "obj_key_backslash_forbidden")
8
- // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
9
- .refine((v) => !/[\u0000-\u001F\u007F]/.test(v), "obj_key_control_chars_forbidden")
10
- .refine((v) => v.length > 0, "obj_key_empty")
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
- (v) => v.split("/").every((s) => /^[a-z0-9._-]+$/.test(s) && s !== "." && s !== ".."),
13
- "obj_key_bad_segments",
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>;
@@ -15,6 +15,7 @@ export class OutlierDetector {
15
15
 
16
16
  isInlier(value: number): boolean {
17
17
  const score = this.zScore.calculate(value);
18
+
18
19
  return Math.abs(score) <= this.threshold;
19
20
  }
20
21
  }