@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/etags.vo.ts CHANGED
@@ -1,7 +1,4 @@
1
- import { z } from "zod/v4";
2
-
3
- const RevisionValue = z.number().int().min(0);
4
- type RevisionValueType = z.infer<typeof RevisionValue>;
1
+ import { RevisionValue, type RevisionValueType } from "./revision-value.vo";
5
2
 
6
3
  type ETagValueType = string;
7
4
 
@@ -28,6 +25,8 @@ export class ETag {
28
25
 
29
26
  export type WeakETagValueType = string;
30
27
 
28
+ export const WeakETagError = { Invalid: "weak.etag.invalid" } as const;
29
+
31
30
  export class WeakETag {
32
31
  static HEADER_NAME = "ETag";
33
32
 
@@ -40,7 +39,7 @@ export class WeakETag {
40
39
  }
41
40
 
42
41
  static fromHeader(value?: WeakETagValueType): WeakETag | null {
43
- if (!value?.startsWith("W/")) throw Error("Invalid WeakETag");
42
+ if (!value?.startsWith("W/")) throw new Error(WeakETagError.Invalid);
44
43
 
45
44
  const candidate = Number(value.split("W/")[1]);
46
45
 
@@ -1,18 +1,25 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const ExtensionTypeError = "extension.not.string" as const;
4
- export const ExtensionEmptyError = "extension.empty" as const;
5
- export const ExtensionTooLongError = "extension.too.long" as const;
6
- export const ExtensionBadCharsError = "extension.bad.chars" as const;
3
+ export const ExtensionError = {
4
+ Type: "extension.type",
5
+ Empty: "extension.empty",
6
+ TooLong: "extension.too.long",
7
+ BadChars: "extension.bad.chars",
8
+ } as const;
9
+
10
+ // Lowercase letters and digits allowed
11
+ const EXTENSION_WHITELIST = /^[a-z0-9]+$/;
12
+
13
+ const LEADING_DOT_FILE = /^\./;
7
14
 
8
15
  export const Extension = z
9
- .string(ExtensionTypeError)
10
- .trim()
16
+ .string(ExtensionError.Type)
11
17
  .toLowerCase()
12
- .transform((value) => (value.startsWith(".") ? value.slice(1) : value))
13
- .refine((value) => value.length >= 1, ExtensionEmptyError)
14
- .refine((value) => value.length <= 16, ExtensionTooLongError)
15
- .refine((value) => /^[a-z0-9]+$/.test(value), ExtensionBadCharsError)
18
+ .min(2, ExtensionError.Empty)
19
+ .max(16, ExtensionError.TooLong)
20
+ // Transform ".png" -> "png"
21
+ .transform((value) => value.replace(LEADING_DOT_FILE, ""))
22
+ .refine((value) => EXTENSION_WHITELIST.test(value), ExtensionError.BadChars)
16
23
  .brand("Extension");
17
24
 
18
25
  export type ExtensionType = z.infer<typeof Extension>;
@@ -0,0 +1,12 @@
1
+ import { z } from "zod/v4";
2
+
3
+ export const FeatureFlagValueError = { Invalid: "feature.flag.value.invalid" } as const;
4
+
5
+ export enum FeatureFlagEnum {
6
+ yes = "yes",
7
+ no = "no",
8
+ }
9
+
10
+ export const FeatureFlagValue = z.enum(FeatureFlagEnum, FeatureFlagValueError.Invalid);
11
+
12
+ export type FeatureFlagValueType = z.infer<typeof FeatureFlagValue>;
@@ -1,12 +1,4 @@
1
- import { z } from "zod/v4";
2
-
3
- export enum FeatureFlagEnum {
4
- yes = "yes",
5
- no = "no",
6
- }
7
-
8
- export const FeatureFlagValue = z.enum(FeatureFlagEnum);
9
- export type FeatureFlagValueType = z.infer<typeof FeatureFlagValue>;
1
+ import { FeatureFlagEnum, type FeatureFlagValueType } from "./feature-flag-value.vo";
10
2
 
11
3
  export class FeatureFlag {
12
4
  static isEnabled(flag: FeatureFlagValueType): boolean {
@@ -2,30 +2,31 @@ 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;
5
+ export const FilePathAbsoluteSchemaError = {
6
+ Type: "file.path.absolute.type",
7
+ LeadingSlash: "file.path.absolute.leading.slash",
8
+ TrailingSlash: "file.path.absolute.trailing.slash",
9
+ BackslashForbidden: "file.path.absolute.backslash.forbidden",
10
+ Empty: "file.path.absolute.empty",
11
+ } as const;
9
12
 
10
13
  export const FilePathAbsoluteSchema = z
11
- .string(AbsFilePathTypeError)
12
- .trim()
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)
14
+ .string(FilePathAbsoluteSchemaError.Type)
15
+ .min(1, FilePathAbsoluteSchemaError.Empty)
16
+ .refine((value) => value.startsWith("/"), FilePathAbsoluteSchemaError.LeadingSlash)
17
+ .refine((value) => !value.endsWith("/"), FilePathAbsoluteSchemaError.TrailingSlash)
18
+ .refine((value) => !value.includes("\\"), FilePathAbsoluteSchemaError.BackslashForbidden)
20
19
  .transform((normalized) => {
21
- const lastSlashIndex = normalized.lastIndexOf("/");
22
- const directoryCandidate = lastSlashIndex === 0 ? "/" : normalized.slice(0, lastSlashIndex);
23
- const filenameCandidate = normalized.slice(lastSlashIndex + 1);
20
+ const index = normalized.lastIndexOf("/");
21
+
22
+ const directoryCandidate = index === 0 ? "/" : normalized.slice(0, index);
23
+ const filenameCandidate = normalized.slice(index + 1);
24
24
 
25
25
  const directory = DirectoryPathAbsoluteSchema.parse(directoryCandidate);
26
26
  const filename = Filename.fromString(filenameCandidate);
27
27
 
28
28
  return { directory, filename };
29
- });
29
+ })
30
+ .brand("FilePathAbsoluteSchema");
30
31
 
31
32
  export type FilePathAbsoluteType = z.infer<typeof FilePathAbsoluteSchema>;
@@ -2,21 +2,23 @@ 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;
5
+ export const FilePathRelativeSchemaError = {
6
+ Type: "file.path.relative.type",
7
+ LeadingSlash: "file.path.relative.leading.slash",
8
+ BackslashForbidden: "file.path.relative.backslash.forbidden",
9
+ RequiresDirectory: "file.path.relative.requires.directory",
10
+ Empty: "file.path.relative.empty",
11
+ } as const;
9
12
 
10
13
  export const FilePathRelativeSchema = z
11
- .string(RelFilePathTypeError)
12
- .trim()
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)
14
+ .string(FilePathRelativeSchemaError.Type)
15
+ .min(1, FilePathRelativeSchemaError.Empty)
16
+ .refine((value) => !value.startsWith("/"), FilePathRelativeSchemaError.LeadingSlash)
17
+ .refine((value) => !value.includes("\\"), FilePathRelativeSchemaError.BackslashForbidden)
18
+ .refine((value) => value.includes("/"), FilePathRelativeSchemaError.RequiresDirectory)
18
19
  .transform((normalized) => {
19
20
  const lastSlashIndex = normalized.lastIndexOf("/");
21
+
20
22
  const directoryCandidate = normalized.slice(0, lastSlashIndex);
21
23
  const filenameCandidate = normalized.slice(lastSlashIndex + 1);
22
24
 
@@ -24,6 +26,7 @@ export const FilePathRelativeSchema = z
24
26
  const filename = Filename.fromString(filenameCandidate);
25
27
 
26
28
  return { directory, filename };
27
- });
29
+ })
30
+ .brand("FilePathRelativeSchema");
28
31
 
29
32
  export type FilePathRelativeType = z.infer<typeof FilePathRelativeSchema>;
@@ -20,10 +20,10 @@ export class FilePathRelative {
20
20
  return new FilePathRelative(directory, filename);
21
21
  }
22
22
 
23
- static fromString(pathCandidate: string): FilePathRelative {
24
- const parsed = FilePathRelativeSchema.parse(pathCandidate);
23
+ static fromString(candidate: string): FilePathRelative {
24
+ const schema = FilePathRelativeSchema.parse(candidate);
25
25
 
26
- return new FilePathRelative(parsed.directory, parsed.filename);
26
+ return new FilePathRelative(schema.directory, schema.filename);
27
27
  }
28
28
 
29
29
  get(): string {
@@ -38,7 +38,7 @@ export class FilePathRelative {
38
38
  return this.filename;
39
39
  }
40
40
 
41
- withDirectoryRelative(newDirectory: DirectoryPathRelativeType): FilePathRelative {
41
+ withDirectory(newDirectory: DirectoryPathRelativeType): FilePathRelative {
42
42
  return new FilePathRelative(newDirectory, this.filename);
43
43
  }
44
44
 
@@ -67,10 +67,10 @@ export class FilePathAbsolute {
67
67
  return new FilePathAbsolute(directory, filename);
68
68
  }
69
69
 
70
- static fromString(pathCandidate: string): FilePathAbsolute {
71
- const parsed = FilePathAbsoluteSchema.parse(pathCandidate);
70
+ static fromString(candidate: string): FilePathAbsolute {
71
+ const schema = FilePathAbsoluteSchema.parse(candidate);
72
72
 
73
- return new FilePathAbsolute(parsed.directory, parsed.filename);
73
+ return new FilePathAbsolute(schema.directory, schema.filename);
74
74
  }
75
75
 
76
76
  get(): string {
@@ -86,7 +86,7 @@ export class FilePathAbsolute {
86
86
  return this.filename;
87
87
  }
88
88
 
89
- withDirectoryAbsolute(newDirectory: DirectoryPathAbsoluteType): FilePathAbsolute {
89
+ withDirectory(newDirectory: DirectoryPathAbsoluteType): FilePathAbsolute {
90
90
  return new FilePathAbsolute(newDirectory, this.filename);
91
91
  }
92
92
 
@@ -2,19 +2,22 @@ import { z } from "zod/v4";
2
2
  import { Basename } from "./basename.vo";
3
3
  import { Extension } from "./extension.vo";
4
4
 
5
- export const FilenameTypeError = "filename.not.string" as const;
6
- export const FilenameInvalidError = "filename.invalid" as const;
5
+ export const FilenameFromStringError = {
6
+ Type: "filename.from.string.type",
7
+ Invalid: "filename.from.string.Invalid",
8
+ } as const;
7
9
 
8
- export const FilenameFromString = z
9
- .string(FilenameTypeError)
10
- .trim()
11
- .refine((value) => {
12
- const index = value.lastIndexOf(".");
10
+ // .+ at least one character, advances to the last dot
11
+ // .
12
+ // .+ at least one character
13
+ const DOT_WITH_SIDES = /^.+\..+$/;
13
14
 
14
- return index > 0 && index < value.length - 1;
15
- }, FilenameInvalidError)
15
+ export const FilenameFromString = z
16
+ .string(FilenameFromStringError.Type)
17
+ .regex(DOT_WITH_SIDES, FilenameFromStringError.Invalid)
16
18
  .transform((value) => {
17
19
  const index = value.lastIndexOf(".");
20
+
18
21
  const basename = Basename.parse(value.slice(0, index));
19
22
  const extension = Extension.parse(value.slice(index + 1));
20
23
 
@@ -1,13 +1,20 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const FilenameSuffixTypeError = "suffix.not.string" as const;
4
- export const FilenameSuffixTooLongError = "suffix_too_long" as const;
3
+ export const FilenameSuffixError = {
4
+ Type: "suffix.type",
5
+ Empty: "suffix.empty",
6
+ TooLong: "suffix.too.long",
7
+ BadChars: "suffix.bad.chars",
8
+ } as const;
9
+
10
+ // Letters, digits, underscores, and hyphens allowed
11
+ const FILENAME_SUFFIX_WHITELIST = /^[a-zA-Z0-9_-]+$/;
5
12
 
6
13
  export const FilenameSuffix = z
7
- .string(FilenameSuffixTypeError)
8
- .trim()
9
- .transform((value) => value.replace(/[^A-Za-z0-9_-]/g, ""))
10
- .refine((value) => value.length <= 32, FilenameSuffixTooLongError)
11
- .brand("basename_suffix");
14
+ .string(FilenameSuffixError.Type)
15
+ .min(1, FilenameSuffixError.Empty)
16
+ .max(32, FilenameSuffixError.TooLong)
17
+ .regex(FILENAME_SUFFIX_WHITELIST, FilenameSuffixError.BadChars)
18
+ .brand("FilenameSuffix");
12
19
 
13
20
  export type FilenameSuffixType = z.infer<typeof FilenameSuffix>;
@@ -18,8 +18,9 @@ export class Filename {
18
18
  }
19
19
 
20
20
  static fromString(candidate: string): Filename {
21
- const { basename, extension } = FilenameFromString.parse(candidate);
22
- return new Filename(basename, extension);
21
+ const filename = FilenameFromString.parse(candidate);
22
+
23
+ return new Filename(filename.basename, filename.extension);
23
24
  }
24
25
 
25
26
  get(): string {
@@ -54,4 +55,12 @@ export class Filename {
54
55
 
55
56
  return new Filename(basename, this.extension);
56
57
  }
58
+
59
+ toString(): string {
60
+ return this.get();
61
+ }
62
+
63
+ toJSON(): string {
64
+ return this.get();
65
+ }
57
66
  }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod/v4";
2
+
3
+ export const HeightMillimetersError = {
4
+ Type: "height.millimeters.type",
5
+ Invalid: "height.millimeters.invalid",
6
+ } as const;
7
+
8
+ export const HeightMillimeters = z
9
+ .number(HeightMillimetersError.Type)
10
+ .int(HeightMillimetersError.Type)
11
+ .min(0, HeightMillimetersError.Invalid)
12
+ .brand("HeightMillimeters");
package/src/height.vo.ts CHANGED
@@ -1,64 +1,20 @@
1
- import { z } from "zod/v4";
1
+ import { HeightMillimeters } from "./height-milimiters.vo";
2
2
  import { RoundToDecimal, RoundToNearest } from "./rounding.adapter";
3
3
  import type { RoundingPort } from "./rounding.port";
4
4
 
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
- );
23
-
24
- export enum HeightUnit {
25
- cm = "cm",
26
- ft_in = "ft_in",
27
- }
28
-
29
5
  export class Height {
30
6
  private static readonly MILLIMETERS_PER_CENTIMETER = 10;
31
- private static readonly MILLIMETERS_PER_INCH = 25.4;
32
- private static readonly INCHES_PER_FOOT = 12;
33
7
 
34
8
  private constructor(private readonly millimeters: number) {}
35
9
 
36
10
  static fromCentimeters(centimeters: number, rounding: RoundingPort = new RoundToNearest()): Height {
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);
11
+ const millimeters = rounding.round(centimeters * Height.MILLIMETERS_PER_CENTIMETER);
41
12
 
42
- return new Height(validatedMillimeters);
13
+ return new Height(HeightMillimeters.parse(millimeters));
43
14
  }
44
15
 
45
- static fromFeetInches(feet: number, inches = 0, rounding: RoundingPort = new RoundToNearest()): Height {
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);
54
- }
55
-
56
- static fromMillimeters(millimeters: number, rounding: RoundingPort = new RoundToNearest()): Height {
57
- const validatedMillimetersInput = HeightNonNegativeQuantity.parse(millimeters);
58
- const millimetersRounded = rounding.round(validatedMillimetersInput);
59
- const validatedMillimeters = HeightCanonicalMillimeters.parse(millimetersRounded);
60
-
61
- return new Height(validatedMillimeters);
16
+ static fromMillimeters(millimeters: number): Height {
17
+ return new Height(HeightMillimeters.parse(millimeters));
62
18
  }
63
19
 
64
20
  static zero(): Height {
@@ -80,44 +36,21 @@ export class Height {
80
36
  return centimeters;
81
37
  }
82
38
 
83
- toFeetInches(rounding: RoundingPort = new RoundToNearest()): { feet: number; inches: number } {
84
- const totalInchesFloat = this.millimeters / Height.MILLIMETERS_PER_INCH;
85
- const totalInchesRounded = rounding.round(totalInchesFloat);
86
- const totalWholeInches = HeightRoundedWholeInches.parse(totalInchesRounded);
39
+ format(rounding?: RoundingPort): string {
40
+ const chosen = rounding ?? new RoundToDecimal(1);
41
+ const value = this.toCentimeters(chosen);
87
42
 
88
- const feet = Math.floor(totalWholeInches / Height.INCHES_PER_FOOT);
89
- const inches = totalWholeInches % Height.INCHES_PER_FOOT;
90
-
91
- return { feet, inches };
92
- }
93
-
94
- format(unit: HeightUnit, rounding?: RoundingPort): string {
95
- if (unit === HeightUnit.cm) {
96
- const chosen = rounding ?? new RoundToDecimal(1);
97
- const value = this.toCentimeters(chosen);
98
-
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}″`;
43
+ return `${value} cm`;
106
44
  }
107
45
 
108
46
  toString(): string {
109
- return this.format(HeightUnit.cm, new RoundToDecimal(1));
47
+ return this.format(new RoundToDecimal(1));
110
48
  }
111
49
 
112
50
  equals(another: Height): boolean {
113
51
  return this.millimeters === another.millimeters;
114
52
  }
115
53
 
116
- compare(another: Height): -1 | 0 | 1 {
117
- if (this.equals(another)) return 0;
118
- return this.millimeters < another.millimeters ? -1 : 1;
119
- }
120
-
121
54
  greaterThan(another: Height): boolean {
122
55
  return this.millimeters > another.millimeters;
123
56
  }
@@ -130,11 +63,7 @@ export class Height {
130
63
  return this.millimeters === 0;
131
64
  }
132
65
 
133
- toJSON(): { mm: number } {
134
- return { mm: this.millimeters };
135
- }
136
-
137
- static fromJSON(input: { mm: number }): Height {
138
- return Height.fromMillimeters(input.mm);
66
+ toJSON(): number {
67
+ return this.millimeters;
139
68
  }
140
69
  }
@@ -12,8 +12,9 @@ export const HourFormatters: Record<HourFormatterEnum, HourFormatter> = {
12
12
  TWENTY_FOUR_HOURS: (value) => value.toString().padStart(2, "0"),
13
13
  TWENTY_FOUR_HOURS_WO_PADDING: (value) => value.toString(),
14
14
  AM_PM: (value) => {
15
- const twelveHourValue = value % 12 || 12;
16
15
  const suffix = value < 12 ? "a.m." : "p.m.";
16
+ const twelveHourValue = value % 12 || 12;
17
+
17
18
  return `${twelveHourValue} ${suffix}`;
18
19
  },
19
20
  TWELVE_HOURS: (value) => (value % 12 || 12).toString().padStart(2, "0"),
@@ -0,0 +1,12 @@
1
+ import { z } from "zod/v4";
2
+
3
+ export const HourSchemaError = { Type: "hour.schema.error", Invalid: "hour.schema.invalid" };
4
+
5
+ export const HourSchema = z
6
+ .number(HourSchemaError.Type)
7
+ .int(HourSchemaError.Type)
8
+ .gte(0, HourSchemaError.Invalid)
9
+ .lte(23, HourSchemaError.Invalid)
10
+ .brand("HourSchema");
11
+
12
+ export type HourSchemaType = z.infer<typeof HourSchema>;
package/src/hour.vo.ts CHANGED
@@ -1,33 +1,25 @@
1
1
  import { type HourFormatter, HourFormatters } from "./hour-format.service";
2
+ import { HourSchema, type HourSchemaType } from "./hour-schema.vo";
2
3
  import type { TimestampType } from "./timestamp.vo";
3
4
 
4
- export const HourValueError = "invalid.hour" as const;
5
-
6
5
  export class Hour {
7
- private readonly value: number;
6
+ private readonly value: HourSchemaType;
8
7
 
9
8
  static readonly ZERO = new Hour(0);
10
9
  static readonly MAX = new Hour(23);
11
10
 
12
11
  constructor(candidate: number) {
13
- if (!Number.isInteger(candidate) || candidate < 0 || candidate >= 24) {
14
- throw new Error(HourValueError);
15
- }
16
- this.value = candidate;
12
+ this.value = HourSchema.parse(candidate);
17
13
  }
18
14
 
19
15
  static fromEpochMs(timestamp: TimestampType): Hour {
20
16
  return new Hour(new Date(timestamp).getUTCHours());
21
17
  }
22
18
 
23
- get(): number {
19
+ get(): HourSchemaType {
24
20
  return this.value;
25
21
  }
26
22
 
27
- toString(): string {
28
- return HourFormatters.TWENTY_FOUR_HOURS(this.value);
29
- }
30
-
31
23
  format(formatter: HourFormatter): string {
32
24
  return formatter(this.value);
33
25
  }
@@ -47,4 +39,12 @@ export class Hour {
47
39
  static list(): readonly Hour[] {
48
40
  return Array.from({ length: 24 }, (_, index) => new Hour(index));
49
41
  }
42
+
43
+ toString(): string {
44
+ return HourFormatters.TWENTY_FOUR_HOURS(this.value);
45
+ }
46
+
47
+ toJSON(): number {
48
+ return this.value;
49
+ }
50
50
  }
@@ -1,16 +1,14 @@
1
1
  import type { IBAN } from "./iban.vo";
2
2
 
3
- type IbanMaskedType = string;
4
-
5
3
  export class IbanMask {
6
- static censor(iban: IBAN): IbanMaskedType {
4
+ static censor(iban: IBAN): string {
7
5
  const value = iban.format();
8
6
 
9
7
  const FIRST_SPACE_INDEX = 4;
10
- const LAST_SPACE_START_INDEX = value.length - 5;
8
+ const LAST_SPACE_INDEX = value.length - 5;
11
9
 
12
10
  const start = value.slice(0, FIRST_SPACE_INDEX);
13
- const middle = value.slice(FIRST_SPACE_INDEX + 1, LAST_SPACE_START_INDEX);
11
+ const middle = value.slice(FIRST_SPACE_INDEX + 1, LAST_SPACE_INDEX);
14
12
  const end = value.slice(-4);
15
13
 
16
14
  const maskedMiddle = middle.replace(/[A-Z0-9]/g, "*");
@@ -0,0 +1,15 @@
1
+ import { z } from "zod/v4";
2
+
3
+ export const IbanSchemaError = { Type: "iban.schema.type", Invalid: "iban.schema.invalid" } as const;
4
+
5
+ // Two letters for country code, two digits, 11–30 uppercase letters or digits
6
+ const IBAN_CHARS_WHITELIST = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/;
7
+
8
+ export const IbanSchema = z
9
+ .string(IbanSchemaError.Type)
10
+ .toUpperCase()
11
+ .transform((value) => value.replaceAll(" ", ""))
12
+ .refine((value) => IBAN_CHARS_WHITELIST.test(value), IbanSchemaError.Invalid)
13
+ .brand("IbanSchema");
14
+
15
+ export type IbanSchemaType = z.infer<typeof IbanSchema>;
package/src/iban.vo.ts CHANGED
@@ -1,37 +1,24 @@
1
- import { z } from "zod/v4";
2
-
3
- export const IBANError = { error: "invalid.iban.format" } as const;
4
-
5
- // 2-letter country code + 2 digits + 11–30 alphanumerics (overall 15–34 chars)
6
- const IBAN_REGEX = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/;
7
-
8
- export const IBANValue = z
9
- .string(IBANError)
10
- .trim()
11
- .toUpperCase()
12
- .transform((val) => val.replace(/\s+/g, ""))
13
- .refine((iban) => IBAN_REGEX.test(iban), IBANError)
14
- .brand("IBAN");
15
-
16
- export type IBANValueType = z.infer<typeof IBANValue>;
17
- export type IBANCountryCode = string;
1
+ import { IbanSchema, type IbanSchemaType } from "./iban-schema.vo";
18
2
 
19
3
  export class IBAN {
20
- private readonly value: IBANValueType;
4
+ private readonly value: IbanSchemaType;
21
5
 
22
- constructor(value: string) {
23
- this.value = IBANValue.parse(value);
6
+ constructor(candidate: string) {
7
+ this.value = IbanSchema.parse(candidate);
24
8
  }
25
9
 
26
- toString(): IBANValueType {
10
+ toString(): IbanSchemaType {
27
11
  return this.value;
28
12
  }
29
13
 
30
14
  format(): string {
15
+ // (.{4}) - capture any four characters
16
+ // (?=.) - positive lookahead, at least one more character after the match
17
+ // "$1 " - replace each match with the group and a space
31
18
  return this.value.replace(/(.{4})(?=.)/g, "$1 ");
32
19
  }
33
20
 
34
- get countryCode(): IBANCountryCode {
21
+ get countryCode(): string {
35
22
  return this.value.slice(0, 2);
36
23
  }
37
24