@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.
Files changed (257) hide show
  1. package/dist/age-years.vo.d.ts +11 -0
  2. package/dist/age-years.vo.js +9 -0
  3. package/dist/age.vo.d.ts +11 -16
  4. package/dist/age.vo.js +20 -31
  5. package/dist/api-key.vo.d.ts +3 -1
  6. package/dist/api-key.vo.js +10 -5
  7. package/dist/basename.vo.d.ts +9 -9
  8. package/dist/basename.vo.js +22 -22
  9. package/dist/clock.vo.d.ts +10 -4
  10. package/dist/clock.vo.js +12 -14
  11. package/dist/date-calculator.service.d.ts +2 -1
  12. package/dist/date-formatter.service.d.ts +3 -4
  13. package/dist/date-range.vo.d.ts +7 -1
  14. package/dist/date-range.vo.js +5 -2
  15. package/dist/day-iso-id.vo.d.ts +5 -2
  16. package/dist/day-iso-id.vo.js +11 -7
  17. package/dist/day.vo.d.ts +4 -3
  18. package/dist/day.vo.js +18 -16
  19. package/dist/directory-path-absolute.vo.d.ts +10 -6
  20. package/dist/directory-path-absolute.vo.js +19 -17
  21. package/dist/directory-path-relative.vo.d.ts +10 -7
  22. package/dist/directory-path-relative.vo.js +18 -17
  23. package/dist/division-factor.vo.d.ts +7 -0
  24. package/dist/division-factor.vo.js +9 -0
  25. package/dist/duration-ms.vo.d.ts +6 -0
  26. package/dist/duration-ms.vo.js +3 -0
  27. package/dist/duration.service.d.ts +2 -14
  28. package/dist/duration.service.js +16 -35
  29. package/dist/email-mask.service.d.ts +1 -6
  30. package/dist/email-mask.service.js +6 -8
  31. package/dist/etags.vo.d.ts +4 -3
  32. package/dist/etags.vo.js +3 -3
  33. package/dist/extension.vo.d.ts +6 -4
  34. package/dist/extension.vo.js +15 -10
  35. package/dist/feature-flag-value.vo.d.ts +10 -0
  36. package/dist/feature-flag-value.vo.js +8 -0
  37. package/dist/feature-flag.vo.d.ts +1 -7
  38. package/dist/feature-flag.vo.js +1 -7
  39. package/dist/file-path-absolute-schema.vo.d.ts +10 -7
  40. package/dist/file-path-absolute-schema.vo.js +17 -17
  41. package/dist/file-path-relative-schema.vo.d.ts +10 -7
  42. package/dist/file-path-relative-schema.vo.js +14 -12
  43. package/dist/file-path.vo.d.ts +4 -4
  44. package/dist/file-path.vo.js +8 -8
  45. package/dist/filename-from-string.vo.d.ts +4 -2
  46. package/dist/filename-from-string.vo.js +10 -8
  47. package/dist/filename-suffix.vo.d.ts +7 -3
  48. package/dist/filename-suffix.vo.js +13 -7
  49. package/dist/filename.vo.d.ts +2 -0
  50. package/dist/filename.vo.js +8 -2
  51. package/dist/height-milimiters.vo.d.ts +6 -0
  52. package/dist/height-milimiters.vo.js +10 -0
  53. package/dist/height.vo.d.ts +3 -20
  54. package/dist/height.vo.js +11 -62
  55. package/dist/hour-format.service.js +1 -1
  56. package/dist/hour-schema.vo.d.ts +7 -0
  57. package/dist/hour-schema.vo.js +8 -0
  58. package/dist/hour.vo.d.ts +4 -3
  59. package/dist/hour.vo.js +8 -8
  60. package/dist/iban-mask.service.d.ts +1 -3
  61. package/dist/iban-mask.service.js +2 -2
  62. package/dist/iban-schema.vo.d.ts +7 -0
  63. package/dist/iban-schema.vo.js +10 -0
  64. package/dist/iban.vo.d.ts +4 -10
  65. package/dist/iban.vo.js +6 -13
  66. package/dist/image.vo.d.ts +6 -4
  67. package/dist/image.vo.js +13 -12
  68. package/dist/index.d.ts +24 -2
  69. package/dist/index.js +24 -2
  70. package/dist/language.vo.d.ts +2 -1
  71. package/dist/language.vo.js +6 -4
  72. package/dist/linear-regression.service.d.ts +27 -0
  73. package/dist/{simple-linear-regression.service.js → linear-regression.service.js} +17 -15
  74. package/dist/mean.service.d.ts +3 -1
  75. package/dist/mean.service.js +3 -4
  76. package/dist/mime-types.vo.d.ts +1 -2
  77. package/dist/mime-value.vo.d.ts +9 -0
  78. package/dist/mime-value.vo.js +9 -0
  79. package/dist/mime.vo.d.ts +11 -17
  80. package/dist/mime.vo.js +10 -27
  81. package/dist/min-max-scaler.service.d.ts +7 -5
  82. package/dist/min-max-scaler.service.js +12 -10
  83. package/dist/minute-schema.vo.d.ts +7 -0
  84. package/dist/minute-schema.vo.js +8 -0
  85. package/dist/minute.vo.d.ts +4 -3
  86. package/dist/minute.vo.js +8 -8
  87. package/dist/money-amount.vo.d.ts +7 -0
  88. package/dist/money-amount.vo.js +7 -0
  89. package/dist/money.vo.d.ts +9 -18
  90. package/dist/money.vo.js +14 -27
  91. package/dist/month-iso-id.vo.d.ts +4 -2
  92. package/dist/month-iso-id.vo.js +13 -7
  93. package/dist/month.vo.d.ts +4 -3
  94. package/dist/month.vo.js +21 -21
  95. package/dist/multiplication-factor.vo.d.ts +7 -0
  96. package/dist/multiplication-factor.vo.js +9 -0
  97. package/dist/object-key.vo.d.ts +9 -6
  98. package/dist/object-key.vo.js +20 -19
  99. package/dist/outlier-detector.service.d.ts +3 -1
  100. package/dist/outlier-detector.service.js +2 -2
  101. package/dist/package-version-schema.vo.d.ts +11 -0
  102. package/dist/package-version-schema.vo.js +15 -0
  103. package/dist/package-version.vo.d.ts +11 -20
  104. package/dist/package-version.vo.js +11 -20
  105. package/dist/pagination-page.vo.d.ts +6 -0
  106. package/dist/pagination-page.vo.js +7 -0
  107. package/dist/pagination-skip.vo.d.ts +7 -0
  108. package/dist/pagination-skip.vo.js +9 -0
  109. package/dist/pagination-take.vo.d.ts +7 -0
  110. package/dist/pagination-take.vo.js +9 -0
  111. package/dist/pagination.service.d.ts +3 -8
  112. package/dist/pagination.service.js +5 -12
  113. package/dist/percentage.service.d.ts +3 -1
  114. package/dist/percentage.service.js +2 -2
  115. package/dist/population-standard-deviation.service.d.ts +3 -1
  116. package/dist/population-standard-deviation.service.js +5 -4
  117. package/dist/quarter-iso-id.vo.d.ts +3 -2
  118. package/dist/quarter-iso-id.vo.js +7 -9
  119. package/dist/quarter.vo.d.ts +2 -1
  120. package/dist/quarter.vo.js +10 -7
  121. package/dist/random.service.d.ts +3 -4
  122. package/dist/random.service.js +5 -11
  123. package/dist/rate-limiter.service.d.ts +2 -2
  124. package/dist/rate-limiter.service.js +8 -8
  125. package/dist/reordering-item-position-value.vo.d.ts +6 -0
  126. package/dist/reordering-item-position-value.vo.js +6 -0
  127. package/dist/reordering.service.d.ts +7 -23
  128. package/dist/reordering.service.js +15 -24
  129. package/dist/revision-value.vo.d.ts +7 -0
  130. package/dist/revision-value.vo.js +6 -0
  131. package/dist/revision.vo.d.ts +6 -13
  132. package/dist/revision.vo.js +10 -22
  133. package/dist/rounding.adapter.d.ts +7 -2
  134. package/dist/rounding.adapter.js +13 -5
  135. package/dist/size-bytes.vo.d.ts +6 -0
  136. package/dist/size-bytes.vo.js +7 -0
  137. package/dist/size.vo.d.ts +15 -15
  138. package/dist/size.vo.js +41 -51
  139. package/dist/stopwatch.service.d.ts +3 -1
  140. package/dist/stopwatch.service.js +2 -2
  141. package/dist/sum.service.js +8 -8
  142. package/dist/thousands-separator.service.js +4 -1
  143. package/dist/time.service.d.ts +8 -0
  144. package/dist/time.service.js +13 -0
  145. package/dist/timestamp.vo.d.ts +1 -1
  146. package/dist/timestamp.vo.js +4 -5
  147. package/dist/timezone.vo.d.ts +4 -1
  148. package/dist/timezone.vo.js +12 -6
  149. package/dist/tsconfig.tsbuildinfo +1 -1
  150. package/dist/week-iso-id.vo.d.ts +4 -2
  151. package/dist/week-iso-id.vo.js +15 -9
  152. package/dist/week.vo.d.ts +4 -3
  153. package/dist/week.vo.js +21 -22
  154. package/dist/weekday.vo.d.ts +1 -1
  155. package/dist/weekday.vo.js +6 -8
  156. package/dist/weight-grams.vo.d.ts +7 -0
  157. package/dist/weight-grams.vo.js +7 -0
  158. package/dist/weight.vo.d.ts +12 -35
  159. package/dist/weight.vo.js +23 -72
  160. package/dist/year-iso-id.vo.d.ts +3 -2
  161. package/dist/year-iso-id.vo.js +6 -4
  162. package/dist/year.vo.d.ts +5 -6
  163. package/dist/year.vo.js +21 -26
  164. package/dist/z-score.service.d.ts +3 -1
  165. package/dist/z-score.service.js +2 -2
  166. package/package.json +4 -4
  167. package/readme.md +21 -2
  168. package/src/age-years.vo.ts +14 -0
  169. package/src/age.vo.ts +22 -35
  170. package/src/api-key.vo.ts +11 -5
  171. package/src/basename.vo.ts +24 -22
  172. package/src/clock.vo.ts +16 -17
  173. package/src/date-calculator.service.ts +2 -1
  174. package/src/date-formatter.service.ts +4 -5
  175. package/src/date-range.vo.ts +6 -2
  176. package/src/day-iso-id.vo.ts +12 -8
  177. package/src/day.vo.ts +27 -24
  178. package/src/directory-path-absolute.vo.ts +23 -18
  179. package/src/directory-path-relative.vo.ts +21 -18
  180. package/src/division-factor.vo.ts +13 -0
  181. package/src/duration-ms.vo.ts +7 -0
  182. package/src/duration.service.ts +16 -40
  183. package/src/email-mask.service.ts +7 -15
  184. package/src/etags.vo.ts +4 -5
  185. package/src/extension.vo.ts +17 -10
  186. package/src/feature-flag-value.vo.ts +12 -0
  187. package/src/feature-flag.vo.ts +1 -9
  188. package/src/file-path-absolute-schema.vo.ts +18 -17
  189. package/src/file-path-relative-schema.vo.ts +15 -12
  190. package/src/file-path.vo.ts +8 -8
  191. package/src/filename-from-string.vo.ts +12 -9
  192. package/src/filename-suffix.vo.ts +14 -7
  193. package/src/filename.vo.ts +11 -2
  194. package/src/height-milimiters.vo.ts +12 -0
  195. package/src/height.vo.ts +12 -83
  196. package/src/hour-format.service.ts +2 -1
  197. package/src/hour-schema.vo.ts +12 -0
  198. package/src/hour.vo.ts +12 -12
  199. package/src/iban-mask.service.ts +3 -5
  200. package/src/iban-schema.vo.ts +15 -0
  201. package/src/iban.vo.ts +9 -22
  202. package/src/image.vo.ts +14 -12
  203. package/src/index.ts +24 -2
  204. package/src/language.vo.ts +7 -4
  205. package/src/linear-regression.service.ts +71 -0
  206. package/src/mean.service.ts +3 -5
  207. package/src/mime-types.vo.ts +1 -3
  208. package/src/mime-value.vo.ts +12 -0
  209. package/src/mime.vo.ts +12 -33
  210. package/src/min-max-scaler.service.ts +13 -11
  211. package/src/minute-schema.vo.ts +12 -0
  212. package/src/minute.vo.ts +12 -12
  213. package/src/money-amount.vo.ts +11 -0
  214. package/src/money.vo.ts +20 -38
  215. package/src/month-iso-id.vo.ts +14 -7
  216. package/src/month.vo.ts +25 -24
  217. package/src/multiplication-factor.vo.ts +13 -0
  218. package/src/object-key.vo.ts +25 -21
  219. package/src/outlier-detector.service.ts +2 -2
  220. package/src/package-version-schema.vo.ts +21 -0
  221. package/src/package-version.vo.ts +17 -33
  222. package/src/pagination-page.vo.ts +11 -0
  223. package/src/pagination-skip.vo.ts +13 -0
  224. package/src/pagination-take.vo.ts +13 -0
  225. package/src/pagination.service.ts +5 -22
  226. package/src/percentage.service.ts +2 -2
  227. package/src/population-standard-deviation.service.ts +5 -4
  228. package/src/quarter-iso-id.vo.ts +7 -10
  229. package/src/quarter.vo.ts +14 -9
  230. package/src/random.service.ts +6 -9
  231. package/src/rate-limiter.service.ts +9 -8
  232. package/src/reordering-item-position-value.vo.ts +10 -0
  233. package/src/reordering.service.ts +19 -28
  234. package/src/revision-value.vo.ts +10 -0
  235. package/src/revision.vo.ts +10 -25
  236. package/src/rounding.adapter.ts +16 -3
  237. package/src/size-bytes.vo.ts +11 -0
  238. package/src/size.vo.ts +43 -54
  239. package/src/stopwatch.service.ts +3 -3
  240. package/src/sum.service.ts +8 -8
  241. package/src/thousands-separator.service.ts +4 -1
  242. package/src/time.service.ts +15 -0
  243. package/src/timestamp.vo.ts +4 -5
  244. package/src/timezone.vo.ts +12 -6
  245. package/src/week-iso-id.vo.ts +16 -12
  246. package/src/week.vo.ts +26 -28
  247. package/src/weekday.vo.ts +6 -9
  248. package/src/weight-grams.vo.ts +11 -0
  249. package/src/weight.vo.ts +28 -85
  250. package/src/year-iso-id.vo.ts +7 -4
  251. package/src/year.vo.ts +27 -33
  252. package/src/z-score.service.ts +2 -2
  253. package/dist/simple-linear-regression.service.d.ts +0 -25
  254. package/dist/streak-calculator.service.d.ts +0 -13
  255. package/dist/streak-calculator.service.js +0 -22
  256. package/src/simple-linear-regression.service.ts +0 -69
  257. 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 { z } from "zod/v4";
2
+ import { AgeYears, AgeYearsConstraints, type AgeYearsType } from "./age-years.vo";
3
3
  import type { TimestampType } from "./timestamp.vo";
4
4
 
5
- export const AgeValueError = { error: "invalid.age" } as 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 = 1;
11
- static readonly MAX = 130;
8
+ static readonly MIN = AgeYearsConstraints.min;
9
+ static readonly MAX = AgeYearsConstraints.max;
12
10
 
13
- static readonly AgeValue = z
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
- private constructor(private readonly value: z.infer<typeof Age.AgeValue>) {}
13
+ static fromValue(candidate: number): Age {
14
+ return new Age(AgeYears.parse(candidate));
15
+ }
21
16
 
22
- get(): number {
23
- return this.value as number;
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
- compare(other: Age): -1 | 0 | 1 {
27
- return this.value === other.value ? 0 : this.value < other.value ? -1 : 1;
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
- static fromValue(candidate: number): Age {
47
- return new Age(Age.AgeValue.parse(candidate));
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 = { error: "invalid.api.key" };
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
- .trim()
8
- .length(64, ApiKeyError)
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>;
@@ -1,29 +1,31 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const BasenameTypeError = "basename.not.string" as const;
4
- export const BasenameEmptyError = "basename.empty" as const;
5
- export const BasenameTooLongError = "basename.too.long" as const;
6
- export const BasenameSlashesForbiddenError = "basename.slashes.forbidden" as const;
7
- export const BasenameControlCharsForbiddenError = "basename.control.chars.forbidden" as const;
8
- export const BasenameDotSegmentsForbiddenError = "basename.dot.segments.forbidden" as const;
9
- export const BasenameDotfilesForbiddenError = "basename.dotfiles.forbidden" as const;
10
- export const BasenameTrailingDotForbiddenError = "basename.trailing.dot.forbidden" as const;
11
- export const BasenameBadCharsError = "basename.bad.chars" as const;
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(BasenameTypeError)
15
- .trim()
16
- .min(1, BasenameEmptyError)
17
- .max(128, BasenameTooLongError)
18
- .refine((s) => !/[/\\]/.test(s), BasenameSlashesForbiddenError)
19
- // dot-related checks: dot-segments first for specific errors…
20
- // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
21
- .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), BasenameControlCharsForbiddenError)
22
- .refine((value) => value !== "." && value !== "..", BasenameDotSegmentsForbiddenError)
23
- // …then any other dotfile
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 = (formatter as ClockFormatter) ?? ClockFormatters.TWENTY_FOUR_HOURS;
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: number; minute: number } {
26
+ get(): { hour: HourSchemaType; minute: MinuteSchemaType } {
25
27
  return { hour: this.hour.get(), minute: this.minute.get() };
26
28
  }
27
29
 
28
- format(formatter?: ClockFormatter): string {
29
- const chosen = formatter ?? this.formatter;
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
- const thisHour = this.hour.get();
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
- const thisHour = this.hour.get();
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: number };
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): FormattedDateType {
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): FormattedDateType {
10
+ static date(date: DateFormattersInputType): string {
12
11
  return format(date, "yyyy/MM/dd");
13
12
  }
14
13
 
15
- static monthDay(date: DateFormattersInputType): FormattedDateType {
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
  }
@@ -1,13 +1,13 @@
1
1
  import type { TimestampType } from "./timestamp.vo";
2
2
 
3
- export const DateRangeInvalidError = "invalid.date.range" as 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(DateRangeInvalidError);
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
  }
@@ -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 = { error: "invalid.day.iso.id" } as const;
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
- export const DayIsoId = z
7
- .string(DayIsoIdError)
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
- return isValid(date) && value === date.toISOString().slice(0, 10);
13
- }, DayIsoIdError);
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 AbsDirTypeError = "abs_dir.not.string" as const;
4
- export const AbsDirMustStartWithSlashError = "abs_dir_must_start_with_slash" as const;
5
- export const AbsDirBackslashForbiddenError = "abs_dir_backslash_forbidden" as const;
6
- export const AbsDirControlCharsForbiddenError = "abs_dir_control_chars_forbidden" as const;
7
- export const AbsDirBadSegmentsError = "abs_dir_bad_segments" as const;
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(AbsDirTypeError)
11
- .trim()
12
- .refine((value) => value.startsWith("/"), AbsDirMustStartWithSlashError)
13
- .refine((value) => !value.includes("\\"), AbsDirBackslashForbiddenError)
14
- // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
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
- }, AbsDirBadSegmentsError)
27
- .brand("directory_path_absolute");
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 RelDirTypeError = "rel_dir.not.string" as const;
4
- export const RelDirMustNotStartWithSlashError = "rel_dir_must_not_start_with_slash" as const;
5
- export const RelDirBackslashForbiddenError = "rel_dir_backslash_forbidden" as const;
6
- export const RelDirControlCharsForbiddenError = "rel_dir_control_chars_forbidden" as const;
7
- export const RelDirEmptyError = "rel_dir_empty" as const;
8
- export const RelDirBadSegmentsError = "rel_dir_bad_segments" as const;
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(RelDirTypeError)
12
- .trim()
13
- .refine((value) => !value.startsWith("/"), RelDirMustNotStartWithSlashError)
14
- .refine((value) => !value.includes("\\"), RelDirBackslashForbiddenError)
15
- // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
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) => /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== ".."),
25
- RelDirBadSegmentsError,
27
+ .every((segment) => DIRECTORY_PATH_RELATIVE_CHARS.test(segment) && !DOT_SEGMENTS.includes(segment)),
28
+ DirectoryPathRelativeError.BadSegments,
26
29
  )
27
- .brand("directory_path_relative");
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>;
@@ -1,29 +1,18 @@
1
- import { z } from "zod/v4";
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 valueMs: DurationMsType;
7
+ private readonly internal: DurationMsType;
19
8
 
20
9
  private static readonly MS_IN_SECOND = 1_000;
21
- private static readonly MS_IN_MINUTE = 60_000;
22
- private static readonly MS_IN_HOUR = 3_600_000;
23
- private static readonly MS_IN_DAY = 86_400_000;
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.valueMs = DurationMsSchema.parse(candidateMs);
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.valueMs / Duration.MS_IN_DAY);
35
+ return Duration.rounding.round(this.internal / Duration.MS_IN_DAY);
47
36
  }
48
37
  get hours(): number {
49
- return Duration.rounding.round(this.valueMs / Duration.MS_IN_HOUR);
38
+ return Duration.rounding.round(this.internal / Duration.MS_IN_HOUR);
50
39
  }
51
40
  get minutes(): number {
52
- return Duration.rounding.round(this.valueMs / Duration.MS_IN_MINUTE);
41
+ return Duration.rounding.round(this.internal / Duration.MS_IN_MINUTE);
53
42
  }
54
43
  get seconds(): number {
55
- return Duration.rounding.round(this.valueMs / Duration.MS_IN_SECOND);
44
+ return Duration.rounding.round(this.internal / Duration.MS_IN_SECOND);
56
45
  }
57
46
  get ms(): DurationMsType {
58
- return this.valueMs;
47
+ return this.internal;
59
48
  }
60
49
 
61
50
  isLongerThan(another: Duration): boolean {
62
- return this.valueMs > another.valueMs;
51
+ return this.internal > another.internal;
63
52
  }
64
53
  isShorterThan(another: Duration): boolean {
65
- return this.valueMs < another.valueMs;
54
+ return this.internal < another.internal;
66
55
  }
67
56
 
68
57
  equals(other: Duration): boolean {
69
- return this.valueMs === other.valueMs;
58
+ return this.internal === other.internal;
70
59
  }
71
60
  add(another: Duration): Duration {
72
- return Duration.Ms(this.valueMs + another.valueMs);
61
+ return Duration.Ms(this.internal + another.internal);
73
62
  }
74
63
  subtract(another: Duration): Duration {
75
- return Duration.Ms(this.valueMs - another.valueMs);
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: EmailType): EmailMaskedType {
10
- const [beforeAt, afterAt] = email.split("@");
2
+ static censor(email: string): string {
3
+ const [local, domain] = email.split("@");
11
4
 
12
- const local = beforeAt as string;
13
- const domain = afterAt as string;
5
+ if (local.length <= 2) return `${"*".repeat(local.length)}@${domain}`;
14
6
 
15
- if (local.length <= 2) {
16
- return `${"*".repeat(local.length)}@${domain}`;
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 = `${local.at(0)}${"*".repeat(local.length - 2)}${local.at(-1)}`;
11
+ const censoredLocal = `${firstCharacter}${censoredPart}${lastCharacter}`;
20
12
 
21
13
  return `${censoredLocal}@${domain}`;
22
14
  }