@bgord/tools 0.14.2 → 0.15.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 (101) hide show
  1. package/dist/age.vo.d.ts +10 -6
  2. package/dist/age.vo.js +23 -17
  3. package/dist/api-key.vo.d.ts +3 -0
  4. package/dist/api-key.vo.js +7 -1
  5. package/dist/clock-format.service.d.ts +9 -0
  6. package/dist/clock-format.service.js +10 -0
  7. package/dist/clock.vo.d.ts +7 -14
  8. package/dist/clock.vo.js +25 -34
  9. package/dist/height.vo.d.ts +7 -7
  10. package/dist/height.vo.js +6 -6
  11. package/dist/hour-format.service.d.ts +10 -0
  12. package/dist/hour-format.service.js +19 -0
  13. package/dist/hour.vo.d.ts +8 -17
  14. package/dist/hour.vo.js +22 -39
  15. package/dist/iban-mask.service.js +6 -3
  16. package/dist/iban.vo.d.ts +6 -4
  17. package/dist/iban.vo.js +7 -5
  18. package/dist/image.vo.d.ts +8 -2
  19. package/dist/image.vo.js +14 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/language.vo.d.ts +4 -1
  23. package/dist/language.vo.js +5 -3
  24. package/dist/mean.service.d.ts +4 -2
  25. package/dist/mean.service.js +9 -5
  26. package/dist/min-max-scaler.service.d.ts +22 -14
  27. package/dist/min-max-scaler.service.js +16 -15
  28. package/dist/minute.vo.d.ts +5 -6
  29. package/dist/minute.vo.js +14 -14
  30. package/dist/money.vo.d.ts +13 -2
  31. package/dist/money.vo.js +18 -13
  32. package/dist/outlier-detector.service.d.ts +2 -1
  33. package/dist/outlier-detector.service.js +5 -3
  34. package/dist/percentage.service.d.ts +3 -2
  35. package/dist/percentage.service.js +3 -2
  36. package/dist/population-standard-deviation.service.d.ts +3 -2
  37. package/dist/population-standard-deviation.service.js +5 -4
  38. package/dist/random.service.d.ts +6 -0
  39. package/dist/random.service.js +12 -6
  40. package/dist/rounding.adapter.d.ts +16 -0
  41. package/dist/{rounding.service.js → rounding.adapter.js} +8 -7
  42. package/dist/rounding.port.d.ts +3 -0
  43. package/dist/rounding.port.js +1 -0
  44. package/dist/simple-linear-regression.service.d.ts +11 -4
  45. package/dist/simple-linear-regression.service.js +39 -30
  46. package/dist/size.vo.js +1 -1
  47. package/dist/stopwatch.service.d.ts +1 -1
  48. package/dist/stopwatch.service.js +3 -4
  49. package/dist/sum.service.d.ts +2 -1
  50. package/dist/sum.service.js +11 -0
  51. package/dist/time.service.js +1 -1
  52. package/dist/timestamp.vo.d.ts +3 -0
  53. package/dist/timestamp.vo.js +7 -1
  54. package/dist/timezone.vo.d.ts +3 -0
  55. package/dist/timezone.vo.js +4 -3
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/dist/visually-unambiguous-characters-generator.service.js +0 -1
  58. package/dist/weight.vo.d.ts +4 -4
  59. package/dist/weight.vo.js +1 -1
  60. package/dist/z-score.service.d.ts +3 -2
  61. package/dist/z-score.service.js +3 -2
  62. package/package.json +3 -3
  63. package/readme.md +4 -2
  64. package/src/age.vo.ts +23 -19
  65. package/src/api-key.vo.ts +9 -1
  66. package/src/clock-format.service.ts +15 -0
  67. package/src/clock.vo.ts +24 -43
  68. package/src/height.vo.ts +12 -11
  69. package/src/hour-format.service.ts +21 -0
  70. package/src/hour.vo.ts +24 -47
  71. package/src/iban-mask.service.ts +8 -3
  72. package/src/iban.vo.ts +10 -8
  73. package/src/image.vo.ts +19 -4
  74. package/src/index.ts +2 -2
  75. package/src/language.vo.ts +7 -3
  76. package/src/mean.service.ts +13 -5
  77. package/src/min-max-scaler.service.ts +39 -24
  78. package/src/minute.vo.ts +18 -15
  79. package/src/money.vo.ts +26 -23
  80. package/src/outlier-detector.service.ts +6 -4
  81. package/src/percentage.service.ts +6 -7
  82. package/src/population-standard-deviation.service.ts +8 -5
  83. package/src/random.service.ts +16 -8
  84. package/src/relative-date.vo.ts +0 -1
  85. package/src/rounding.adapter.ts +33 -0
  86. package/src/rounding.port.ts +3 -0
  87. package/src/simple-linear-regression.service.ts +41 -31
  88. package/src/size.vo.ts +1 -1
  89. package/src/stopwatch.service.ts +4 -6
  90. package/src/sum.service.ts +15 -1
  91. package/src/time.service.ts +1 -1
  92. package/src/timestamp.vo.ts +9 -1
  93. package/src/timezone.vo.ts +15 -15
  94. package/src/visually-unambiguous-characters-generator.service.ts +0 -1
  95. package/src/weight.vo.ts +5 -4
  96. package/src/z-score.service.ts +6 -3
  97. package/dist/dates-of-the-week.vo.d.ts +0 -9
  98. package/dist/dates-of-the-week.vo.js +0 -10
  99. package/dist/rounding.service.d.ts +0 -17
  100. package/src/dates-of-the-week.vo.ts +0 -9
  101. package/src/rounding.service.ts +0 -31
@@ -2,4 +2,15 @@ export class Sum {
2
2
  static of(values) {
3
3
  return values.reduce((sum, x) => sum + x, 0);
4
4
  }
5
+ static precise(values) {
6
+ let runningTotal = 0;
7
+ let roundingCompensation = 0;
8
+ for (const currentValue of values) {
9
+ const correctedAddend = currentValue - roundingCompensation;
10
+ const tentativeTotal = runningTotal + correctedAddend;
11
+ roundingCompensation = tentativeTotal - runningTotal - correctedAddend;
12
+ runningTotal = tentativeTotal;
13
+ }
14
+ return runningTotal;
15
+ }
5
16
  }
@@ -1,4 +1,4 @@
1
- import { RoundToDecimal } from "./rounding.service";
1
+ import { RoundToDecimal } from "./rounding.adapter";
2
2
  const rounding = new RoundToDecimal(2);
3
3
  export class TimeResult {
4
4
  days;
@@ -1,3 +1,6 @@
1
1
  import { z } from "zod/v4";
2
+ export declare const TimestampError: {
3
+ readonly error: "invalid.timestamp";
4
+ };
2
5
  export declare const Timestamp: z.core.$ZodBranded<z.ZodNumber, "Timestamp">;
3
6
  export type TimestampType = z.infer<typeof Timestamp>;
@@ -1,2 +1,8 @@
1
1
  import { z } from "zod/v4";
2
- export const Timestamp = z.number().int().gte(0).brand("Timestamp");
2
+ export const TimestampError = { error: "invalid.timestamp" };
3
+ export const Timestamp = z
4
+ .number(TimestampError)
5
+ .int(TimestampError)
6
+ .gte(0, TimestampError)
7
+ .lte(Number.MAX_SAFE_INTEGER, TimestampError)
8
+ .brand("Timestamp");
@@ -1,3 +1,6 @@
1
1
  import { z } from "zod/v4";
2
+ export declare const TimezoneError: {
3
+ readonly error: "timezone.invalid";
4
+ };
2
5
  export declare const Timezone: z.core.$ZodBranded<z.ZodString, "Timezone">;
3
6
  export type TimezoneType = z.infer<typeof Timezone>;
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod/v4";
2
+ export const TimezoneError = { error: "timezone.invalid" };
2
3
  export const Timezone = z
3
- .string()
4
- .min(1)
4
+ .string(TimezoneError)
5
+ .min(1, TimezoneError)
5
6
  .refine((value) => {
6
7
  try {
7
8
  const date = new Date();
@@ -12,5 +13,5 @@ export const Timezone = z
12
13
  catch (_error) {
13
14
  return false;
14
15
  }
15
- }, { message: "timezone.invalid" })
16
+ }, TimezoneError)
16
17
  .brand("Timezone");
@@ -1 +1 @@
1
- {"root":["../src/age.vo.ts","../src/api-key.vo.ts","../src/basename.vo.ts","../src/clock.vo.ts","../src/date-calculator.service.ts","../src/date-formatter.service.ts","../src/date-range.vo.ts","../src/dates-of-the-week.vo.ts","../src/day-iso-id.vo.ts","../src/day.vo.ts","../src/directory-path-absolute.vo.ts","../src/directory-path-relative.vo.ts","../src/dll.service.ts","../src/email-mask.service.ts","../src/etags.vo.ts","../src/extension.vo.ts","../src/feature-flag.vo.ts","../src/file-path-absolute-schema.vo.ts","../src/file-path-relative-schema.vo.ts","../src/file-path.vo.ts","../src/filename-from-string.vo.ts","../src/filename-suffix.vo.ts","../src/filename.vo.ts","../src/height.vo.ts","../src/hour.vo.ts","../src/iban-mask.service.ts","../src/iban.vo.ts","../src/image.vo.ts","../src/index.ts","../src/language.vo.ts","../src/mean.service.ts","../src/mime-types.vo.ts","../src/mime.vo.ts","../src/min-max-scaler.service.ts","../src/minute.vo.ts","../src/money.vo.ts","../src/month-iso-id.vo.ts","../src/month.vo.ts","../src/noop.service.ts","../src/notification-template.vo.ts","../src/object-key.vo.ts","../src/outlier-detector.service.ts","../src/package-version.vo.ts","../src/pagination.service.ts","../src/percentage.service.ts","../src/population-standard-deviation.service.ts","../src/quarter-iso-id.vo.ts","../src/quarter.vo.ts","../src/random.service.ts","../src/rate-limiter.service.ts","../src/relative-date.vo.ts","../src/reordering.service.ts","../src/revision.vo.ts","../src/rounding.service.ts","../src/simple-linear-regression.service.ts","../src/size.vo.ts","../src/stepper.service.ts","../src/stopwatch.service.ts","../src/streak-calculator.service.ts","../src/sum.service.ts","../src/thousands-separator.service.ts","../src/time-zone-offset-value.vo.ts","../src/time.service.ts","../src/timestamp.vo.ts","../src/timezone.vo.ts","../src/ts-utils.ts","../src/visually-unambiguous-characters-generator.service.ts","../src/week-iso-id.vo.ts","../src/week.vo.ts","../src/weekday.vo.ts","../src/weight.vo.ts","../src/year-iso-id.vo.ts","../src/year.vo.ts","../src/z-score.service.ts"],"version":"5.9.2"}
1
+ {"root":["../src/age.vo.ts","../src/api-key.vo.ts","../src/basename.vo.ts","../src/clock-format.service.ts","../src/clock.vo.ts","../src/date-calculator.service.ts","../src/date-formatter.service.ts","../src/date-range.vo.ts","../src/day-iso-id.vo.ts","../src/day.vo.ts","../src/directory-path-absolute.vo.ts","../src/directory-path-relative.vo.ts","../src/dll.service.ts","../src/email-mask.service.ts","../src/etags.vo.ts","../src/extension.vo.ts","../src/feature-flag.vo.ts","../src/file-path-absolute-schema.vo.ts","../src/file-path-relative-schema.vo.ts","../src/file-path.vo.ts","../src/filename-from-string.vo.ts","../src/filename-suffix.vo.ts","../src/filename.vo.ts","../src/height.vo.ts","../src/hour-format.service.ts","../src/hour.vo.ts","../src/iban-mask.service.ts","../src/iban.vo.ts","../src/image.vo.ts","../src/index.ts","../src/language.vo.ts","../src/mean.service.ts","../src/mime-types.vo.ts","../src/mime.vo.ts","../src/min-max-scaler.service.ts","../src/minute.vo.ts","../src/money.vo.ts","../src/month-iso-id.vo.ts","../src/month.vo.ts","../src/noop.service.ts","../src/notification-template.vo.ts","../src/object-key.vo.ts","../src/outlier-detector.service.ts","../src/package-version.vo.ts","../src/pagination.service.ts","../src/percentage.service.ts","../src/population-standard-deviation.service.ts","../src/quarter-iso-id.vo.ts","../src/quarter.vo.ts","../src/random.service.ts","../src/rate-limiter.service.ts","../src/relative-date.vo.ts","../src/reordering.service.ts","../src/revision.vo.ts","../src/rounding.adapter.ts","../src/rounding.port.ts","../src/simple-linear-regression.service.ts","../src/size.vo.ts","../src/stepper.service.ts","../src/stopwatch.service.ts","../src/streak-calculator.service.ts","../src/sum.service.ts","../src/thousands-separator.service.ts","../src/time-zone-offset-value.vo.ts","../src/time.service.ts","../src/timestamp.vo.ts","../src/timezone.vo.ts","../src/ts-utils.ts","../src/visually-unambiguous-characters-generator.service.ts","../src/week-iso-id.vo.ts","../src/week.vo.ts","../src/weekday.vo.ts","../src/weight.vo.ts","../src/year-iso-id.vo.ts","../src/year.vo.ts","../src/z-score.service.ts"],"version":"5.9.3"}
@@ -1,6 +1,5 @@
1
1
  import { Random } from "./random.service";
2
2
  export class VisuallyUnambiguousCharactersGenerator {
3
- // prettier-ignore
4
3
  static chars = [
5
4
  "a",
6
5
  "b",
@@ -1,4 +1,4 @@
1
- import { type RoundingStrategy } from "./rounding.service";
1
+ import type { RoundingPort } from "./rounding.port";
2
2
  export declare enum WeightUnit {
3
3
  kg = "kg",
4
4
  lb = "lb"
@@ -14,9 +14,9 @@ export declare class Weight {
14
14
  static fromGrams(grams: number): Weight;
15
15
  static zero(): Weight;
16
16
  toGrams(): number;
17
- toKilograms(rounding?: RoundingStrategy): number;
18
- toPounds(rounding?: RoundingStrategy): number;
19
- format(unit: WeightUnit, rounding?: RoundingStrategy): string;
17
+ toKilograms(rounding?: RoundingPort): number;
18
+ toPounds(rounding?: RoundingPort): number;
19
+ format(unit: WeightUnit, rounding?: RoundingPort): string;
20
20
  add(other: Weight): Weight;
21
21
  subtract(other: Weight): Weight;
22
22
  multiply(factor: number): Weight;
package/dist/weight.vo.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod/v4";
2
- import { RoundToDecimal } from "./rounding.service";
2
+ import { RoundToDecimal } from "./rounding.adapter";
3
3
  const FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
4
4
  const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
5
5
  const PositiveNumericValue = FiniteNumericValue.gt(0, { message: "Must be greater than 0" });
@@ -1,8 +1,9 @@
1
- import { type RoundingStrategy } from "./rounding.service";
1
+ import type { RoundingPort } from "./rounding.port";
2
+ export declare const ZScoreMinValuesError: "zscore.min.values";
2
3
  export declare class ZScore {
3
4
  private readonly rounding;
4
5
  private readonly mean;
5
6
  private readonly standardDeviation;
6
- constructor(values: number[], rounding?: RoundingStrategy);
7
+ constructor(values: number[], rounding?: RoundingPort);
7
8
  calculate(value: number): number;
8
9
  }
@@ -1,6 +1,7 @@
1
1
  import { Mean } from "./mean.service";
2
2
  import { PopulationStandardDeviation } from "./population-standard-deviation.service";
3
- import { RoundToDecimal } from "./rounding.service";
3
+ import { RoundToDecimal } from "./rounding.adapter";
4
+ export const ZScoreMinValuesError = "zscore.min.values";
4
5
  export class ZScore {
5
6
  rounding;
6
7
  mean;
@@ -8,7 +9,7 @@ export class ZScore {
8
9
  constructor(values, rounding = new RoundToDecimal(2)) {
9
10
  this.rounding = rounding;
10
11
  if (values.length < 2)
11
- throw new Error("At least two values are needed");
12
+ throw new Error(ZScoreMinValuesError);
12
13
  this.mean = Mean.calculate(values);
13
14
  this.standardDeviation = PopulationStandardDeviation.calculate(values);
14
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgord/tools",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Bartosz Gordon",
@@ -21,7 +21,7 @@
21
21
  "preinstall": "bunx only-allow bun"
22
22
  },
23
23
  "devDependencies": {
24
- "@biomejs/biome": "2.2.4",
24
+ "@biomejs/biome": "2.2.5",
25
25
  "@commitlint/cli": "20.1.0",
26
26
  "@commitlint/config-conventional": "20.0.0",
27
27
  "@types/bun": "1.2.23",
@@ -31,7 +31,7 @@
31
31
  "lefthook": "1.13.6",
32
32
  "only-allow": "1.2.1",
33
33
  "shellcheck": "4.1.0",
34
- "typescript": "5.9.2",
34
+ "typescript": "5.9.3",
35
35
  "zod": "4.1.11"
36
36
  },
37
37
  "dependencies": {
package/readme.md CHANGED
@@ -27,11 +27,11 @@ src/
27
27
  ├── age.vo.ts
28
28
  ├── api-key.vo.ts
29
29
  ├── basename.vo.ts
30
+ ├── clock-format.service.ts
30
31
  ├── clock.vo.ts
31
32
  ├── date-calculator.service.ts
32
33
  ├── date-formatter.service.ts
33
34
  ├── date-range.vo.ts
34
- ├── dates-of-the-week.vo.ts
35
35
  ├── day-iso-id.vo.ts
36
36
  ├── day.vo.ts
37
37
  ├── directory-path-absolute.vo.ts
@@ -48,6 +48,7 @@ src/
48
48
  ├── filename-suffix.vo.ts
49
49
  ├── filename.vo.ts
50
50
  ├── height.vo.ts
51
+ ├── hour-format.service.ts
51
52
  ├── hour.vo.ts
52
53
  ├── iban-mask.service.ts
53
54
  ├── iban.vo.ts
@@ -76,7 +77,8 @@ src/
76
77
  ├── relative-date.vo.ts
77
78
  ├── reordering.service.ts
78
79
  ├── revision.vo.ts
79
- ├── rounding.service.ts
80
+ ├── rounding.adapter.ts
81
+ ├── rounding.port.ts
80
82
  ├── simple-linear-regression.service.ts
81
83
  ├── size.vo.ts
82
84
  ├── stepper.service.ts
package/src/age.vo.ts CHANGED
@@ -2,7 +2,9 @@ import { differenceInYears } from "date-fns";
2
2
  import { z } from "zod/v4";
3
3
  import type { TimestampType } from "./timestamp.vo";
4
4
 
5
- export const AgeValueError = { error: "invalid.age" };
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;
6
8
 
7
9
  export class Age {
8
10
  static readonly MIN = 1;
@@ -25,41 +27,43 @@ export class Age {
25
27
  return this.value === other.value ? 0 : this.value < other.value ? -1 : 1;
26
28
  }
27
29
 
28
- equals(another: Age): boolean {
29
- return this.compare(another) === 0;
30
+ equals(other: Age): boolean {
31
+ return this.value === other.value;
30
32
  }
31
33
 
32
- isOlderThan(another: Age): boolean {
33
- return this.compare(another) === 1;
34
+ isOlderThan(other: Age): boolean {
35
+ return this.value > other.value;
34
36
  }
35
37
 
36
- isYoungerThan(another: Age): boolean {
37
- return this.compare(another) === -1;
38
+ isYoungerThan(other: Age): boolean {
39
+ return this.value < other.value;
38
40
  }
39
41
 
40
- isAdult(minAge: Age): boolean {
41
- return this.equals(minAge) || this.isOlderThan(minAge);
42
+ isAdult(minimumAge: Age): boolean {
43
+ return this.value >= minimumAge.value;
42
44
  }
43
45
 
44
46
  static fromValue(candidate: number): Age {
45
47
  return new Age(Age.AgeValue.parse(candidate));
46
48
  }
47
49
 
48
- static fromBirthdateTimestamp(params: { birthdate: TimestampType; now: TimestampType }): Age {
49
- if (params.birthdate > params.now) throw new Error("invalid.birthdate_in_future");
50
-
51
- const years = differenceInYears(new Date(params.now), new Date(params.birthdate));
52
-
53
- return Age.fromValue(years);
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));
54
53
  }
55
54
 
56
55
  static fromBirthdate(params: { birthdate: string; now: TimestampType }): Age {
57
- const birthdate = new Date(params.birthdate).getTime();
56
+ const birthdateMs = new Date(params.birthdate).getTime();
58
57
 
59
- if (birthdate > params.now) throw new Error("invalid.birthdate_in_future");
58
+ if (birthdateMs > params.now) throw new Error(InvalidBirthdateInFutureError);
59
+ return Age.fromValue(differenceInYears(params.now, birthdateMs));
60
+ }
60
61
 
61
- const years = differenceInYears(new Date(params.now), new Date(birthdate));
62
+ toJSON(): number {
63
+ return this.get();
64
+ }
62
65
 
63
- return Age.fromValue(years);
66
+ toString(): string {
67
+ return String(this.value);
64
68
  }
65
69
  }
package/src/api-key.vo.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const ApiKey = z.string().trim().length(64).brand("ApiKey");
3
+ export const ApiKeyError = { error: "invalid.api.key" };
4
+
5
+ export const ApiKey = z
6
+ .string(ApiKeyError)
7
+ .trim()
8
+ .length(64, ApiKeyError)
9
+ .regex(/^[0-9a-f]{64}$/i, ApiKeyError)
10
+ .brand("ApiKey");
11
+
4
12
  export type ApiKeyType = z.infer<typeof ApiKey>;
@@ -0,0 +1,15 @@
1
+ import type { Hour } from "./hour.vo";
2
+ import { HourFormatters } from "./hour-format.service";
3
+ import type { Minute } from "./minute.vo";
4
+
5
+ export type ClockFormatter = (hour: Hour, minute: Minute) => string;
6
+
7
+ enum ClockFormatterEnum {
8
+ TWENTY_FOUR_HOURS = "TWENTY_FOUR_HOURS",
9
+ TWELVE_HOURS = "TWELVE_HOURS",
10
+ }
11
+
12
+ export const ClockFormatters: Record<ClockFormatterEnum, ClockFormatter> = {
13
+ TWENTY_FOUR_HOURS: (hour, minute) => `${hour.toString()}:${minute.toString()}`,
14
+ TWELVE_HOURS: (hour, minute) => `${hour.format(HourFormatters.TWELVE_HOURS)}:${minute.toString()}`,
15
+ } as const;
package/src/clock.vo.ts CHANGED
@@ -1,21 +1,8 @@
1
- import { Hour, HourFormatters } from "./hour.vo";
1
+ import { type ClockFormatter, ClockFormatters } from "./clock-format.service";
2
+ import { Hour } from "./hour.vo";
2
3
  import { Minute } from "./minute.vo";
3
4
  import type { TimestampType } from "./timestamp.vo";
4
5
 
5
- export type ClockFormatter = (hour: Hour, minute: Minute) => string;
6
-
7
- enum ClockFormatterEnum {
8
- TWENTY_FOUR_HOURS = "TWENTY_FOUR_HOURS",
9
- TWELVE_HOURS = "TWELVE_HOURS",
10
- }
11
-
12
- export const ClockFormatters: Record<ClockFormatterEnum, ClockFormatter> = {
13
- TWENTY_FOUR_HOURS: (hour, minute) => `${hour.get().formatted}:${minute.get().formatted}`,
14
-
15
- TWELVE_HOURS: (hour, minute) =>
16
- `${hour.get(HourFormatters.TWELVE_HOURS).formatted}:${minute.get().formatted}`,
17
- } as const;
18
-
19
6
  export class Clock {
20
7
  private readonly formatter: ClockFormatter;
21
8
 
@@ -27,48 +14,42 @@ export class Clock {
27
14
  this.formatter = (formatter as ClockFormatter) ?? ClockFormatters.TWENTY_FOUR_HOURS;
28
15
  }
29
16
 
30
- static fromUtcTimestamp(timestamp: TimestampType, formatter?: ClockFormatter) {
31
- const hour = Hour.fromUtcTimestamp(timestamp);
32
- const minute = Minute.fromUtcTimestamp(timestamp);
17
+ static fromEpochMs(timestamp: TimestampType, formatter?: ClockFormatter): Clock {
18
+ const hour = Hour.fromEpochMs(timestamp);
19
+ const minute = Minute.fromEpochMs(timestamp);
33
20
  return new Clock(hour, minute, formatter);
34
21
  }
35
22
 
36
- get(formatter?: ClockFormatter) {
37
- const format = formatter ?? this.formatter;
23
+ get(): { hour: number; minute: number } {
24
+ return { hour: this.hour.get(), minute: this.minute.get() };
25
+ }
26
+
27
+ format(formatter?: ClockFormatter): string {
28
+ const chosen = formatter ?? this.formatter;
29
+ return chosen(this.hour, this.minute);
30
+ }
38
31
 
39
- return {
40
- raw: { hour: this.hour.get().raw, minute: this.minute.get().raw },
41
- formatted: format(this.hour, this.minute),
42
- };
32
+ toString(): string {
33
+ return this.format();
43
34
  }
44
35
 
45
36
  equals(another: Clock): boolean {
46
- return (
47
- this.hour.get().raw === another.get().raw.hour && this.minute.get().raw === another.get().raw.minute
48
- );
37
+ return this.hour.get() === another.hour.get() && this.minute.get() === another.minute.get();
49
38
  }
50
39
 
51
40
  isAfter(another: Clock): boolean {
52
- if (this.hour.get().raw > another.hour.get().raw) {
53
- return true;
54
- }
41
+ const thisHour = this.hour.get();
42
+ const otherHour = another.hour.get();
55
43
 
56
- if (this.hour.get().raw === another.hour.get().raw && this.minute.get().raw > another.minute.get().raw) {
57
- return true;
58
- }
59
-
60
- return false;
44
+ if (thisHour !== otherHour) return thisHour > otherHour;
45
+ return this.minute.get() > another.minute.get();
61
46
  }
62
47
 
63
48
  isBefore(another: Clock): boolean {
64
- if (this.hour.get().raw < another.hour.get().raw) {
65
- return true;
66
- }
67
-
68
- if (this.hour.get().raw === another.hour.get().raw && this.minute.get().raw < another.minute.get().raw) {
69
- return true;
70
- }
49
+ const thisHour = this.hour.get();
50
+ const otherHour = another.hour.get();
71
51
 
72
- return false;
52
+ if (thisHour !== otherHour) return thisHour < otherHour;
53
+ return this.minute.get() < another.minute.get();
73
54
  }
74
55
  }
package/src/height.vo.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod/v4";
2
- import { type RoundingStrategy, RoundToDecimal, RoundToNearest } from "./rounding.service";
2
+ import { RoundToDecimal, RoundToNearest } from "./rounding.adapter";
3
+ import type { RoundingPort } from "./rounding.port";
3
4
 
4
5
  const FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
5
6
  const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
@@ -22,7 +23,7 @@ export class Height {
22
23
 
23
24
  private constructor(private readonly millimeters: number) {}
24
25
 
25
- static fromCentimeters(centimeters: number, rounding: RoundingStrategy = new RoundToNearest()): Height {
26
+ static fromCentimeters(centimeters: number, rounding: RoundingPort = new RoundToNearest()): Height {
26
27
  NonNegativeNumericValue.parse(centimeters);
27
28
  const mmFloat = centimeters * Height.MILLIMETERS_PER_CENTIMETER;
28
29
  const mmRounded = rounding.round(mmFloat);
@@ -30,7 +31,7 @@ export class Height {
30
31
  return new Height(mmRounded);
31
32
  }
32
33
 
33
- static fromFeetInches(feet: number, inches = 0, rounding: RoundingStrategy = new RoundToNearest()): Height {
34
+ static fromFeetInches(feet: number, inches = 0, rounding: RoundingPort = new RoundToNearest()): Height {
34
35
  NonNegativeNumericValue.parse(feet);
35
36
  NonNegativeNumericValue.parse(inches);
36
37
  const totalInches = feet * Height.INCHES_PER_FOOT + inches;
@@ -40,7 +41,7 @@ export class Height {
40
41
  return new Height(mmRounded);
41
42
  }
42
43
 
43
- static fromMillimeters(millimeters: number, rounding: RoundingStrategy = new RoundToNearest()): Height {
44
+ static fromMillimeters(millimeters: number, rounding: RoundingPort = new RoundToNearest()): Height {
44
45
  NonNegativeNumericValue.parse(millimeters);
45
46
  const mmRounded = rounding.round(millimeters);
46
47
  NonNegativeIntegerMillimeters.parse(mmRounded);
@@ -55,12 +56,12 @@ export class Height {
55
56
  return this.millimeters;
56
57
  }
57
58
 
58
- toCentimeters(rounding?: RoundingStrategy): number {
59
+ toCentimeters(rounding?: RoundingPort): number {
59
60
  const cm = this.millimeters / Height.MILLIMETERS_PER_CENTIMETER;
60
61
  return rounding ? rounding.round(cm) : cm;
61
62
  }
62
63
 
63
- toFeetInches(rounding: RoundingStrategy = new RoundToNearest()): {
64
+ toFeetInches(rounding: RoundingPort = new RoundToNearest()): {
64
65
  feet: number;
65
66
  inches: number;
66
67
  } {
@@ -74,16 +75,16 @@ export class Height {
74
75
  return { feet, inches };
75
76
  }
76
77
 
77
- format(unit: HeightUnit, roundingStrategy?: RoundingStrategy): string {
78
+ format(unit: HeightUnit, rounding?: RoundingPort): string {
78
79
  return {
79
80
  [HeightUnit.cm]: () => {
80
- const rounding = roundingStrategy ?? new RoundToDecimal(1);
81
+ const chosen = rounding ?? new RoundToDecimal(1);
81
82
 
82
- return `${this.toCentimeters(rounding)} cm`;
83
+ return `${this.toCentimeters(chosen)} cm`;
83
84
  },
84
85
  [HeightUnit.ft_in]: () => {
85
- const rounding = roundingStrategy ?? new RoundToNearest();
86
- const { feet, inches } = this.toFeetInches(rounding);
86
+ const chosen = rounding ?? new RoundToNearest();
87
+ const { feet, inches } = this.toFeetInches(chosen);
87
88
 
88
89
  return `${feet}′${inches}″`;
89
90
  },
@@ -0,0 +1,21 @@
1
+ export type HourFormatter = (value: number) => string;
2
+
3
+ enum HourFormatterEnum {
4
+ TWENTY_FOUR_HOURS = "TWENTY_FOUR_HOURS",
5
+ TWENTY_FOUR_HOURS_WO_PADDING = "TWENTY_FOUR_HOURS_WO_PADDING",
6
+ AM_PM = "AM_PM",
7
+ TWELVE_HOURS = "TWELVE_HOURS",
8
+ TWELVE_HOURS_WO_PADDING = "TWELVE_HOURS_WO_PADDING",
9
+ }
10
+
11
+ export const HourFormatters: Record<HourFormatterEnum, HourFormatter> = {
12
+ TWENTY_FOUR_HOURS: (value) => value.toString().padStart(2, "0"),
13
+ TWENTY_FOUR_HOURS_WO_PADDING: (value) => value.toString(),
14
+ AM_PM: (value) => {
15
+ const twelveHourValue = value % 12 || 12;
16
+ const suffix = value < 12 ? "a.m." : "p.m.";
17
+ return `${twelveHourValue} ${suffix}`;
18
+ },
19
+ TWELVE_HOURS: (value) => (value % 12 || 12).toString().padStart(2, "0"),
20
+ TWELVE_HOURS_WO_PADDING: (value) => (value % 12 || 12).toString(),
21
+ } as const;
package/src/hour.vo.ts CHANGED
@@ -1,73 +1,50 @@
1
+ import { type HourFormatter, HourFormatters } from "./hour-format.service";
1
2
  import type { TimestampType } from "./timestamp.vo";
2
3
 
3
- export type HourFormatter = (value: Hour["value"]) => string;
4
-
5
- export enum HourFormatterEnum {
6
- TWENTY_FOUR_HOURS = "TWENTY_FOUR_HOURS",
7
- TWENTY_FOUR_HOURS_WO_PADDING = "TWENTY_FOUR_HOURS_WO_PADDING",
8
- AM_PM = "AM_PM",
9
- TWELVE_HOURS = "TWELVE_HOURS",
10
- TWELVE_HOURS_WO_PADDING = "TWELVE_HOURS_WO_PADDING",
11
- }
12
-
13
- export const HourFormatters: Record<HourFormatterEnum, HourFormatter> = {
14
- TWENTY_FOUR_HOURS: (value) => value.toString().padStart(2, "0"),
15
-
16
- TWENTY_FOUR_HOURS_WO_PADDING: (value) => value.toString(),
17
-
18
- AM_PM: (value) => {
19
- const twelveHour = value % 12 || 12;
20
- return `${twelveHour.toString()} ${value < 12 ? "a.m." : "p.m."}`;
21
- },
22
-
23
- TWELVE_HOURS: (value) => (value % 12 || 12).toString().padStart(2, "0"),
24
-
25
- TWELVE_HOURS_WO_PADDING: (value) => (value % 12 || 12).toString(),
26
- } as const;
4
+ export const HourValueError = "invalid.hour" as const;
27
5
 
28
6
  export class Hour {
29
7
  private readonly value: number;
30
8
 
31
- private readonly formatter: HourFormatter;
32
-
33
9
  static readonly ZERO = new Hour(0);
34
-
35
10
  static readonly MAX = new Hour(23);
36
11
 
37
- constructor(candidate: number, formatter?: HourFormatter) {
38
- if (!Number.isInteger(candidate)) throw new Error("Invalid hour");
39
- if (candidate < 0) throw new Error("Invalid hour");
40
- if (candidate >= 24) throw new Error("Invalid hour");
41
-
12
+ constructor(candidate: number) {
13
+ if (!Number.isInteger(candidate) || candidate < 0 || candidate >= 24) {
14
+ throw new Error(HourValueError);
15
+ }
42
16
  this.value = candidate;
43
- this.formatter = (formatter as HourFormatter) ?? HourFormatters.TWENTY_FOUR_HOURS;
44
17
  }
45
18
 
46
- get(formatter?: HourFormatter) {
47
- const format = formatter ?? this.formatter;
19
+ static fromEpochMs(timestamp: TimestampType): Hour {
20
+ return new Hour(new Date(timestamp).getUTCHours());
21
+ }
48
22
 
49
- return { raw: this.value, formatted: format(this.value) };
23
+ get(): number {
24
+ return this.value;
50
25
  }
51
26
 
52
- equals(another: Hour): boolean {
53
- return this.value === another.get().raw;
27
+ toString(): string {
28
+ return HourFormatters.TWENTY_FOUR_HOURS(this.value);
54
29
  }
55
30
 
56
- isAfter(another: Hour): boolean {
57
- return this.value > another.get().raw;
31
+ format(formatter: HourFormatter): string {
32
+ return formatter(this.value);
58
33
  }
59
34
 
60
- isBefore(another: Hour): boolean {
61
- return this.value < another.get().raw;
35
+ equals(another: Hour): boolean {
36
+ return this.value === another.value;
62
37
  }
63
38
 
64
- static fromUtcTimestamp(timestamp: TimestampType, formatter?: HourFormatter): Hour {
65
- const hours = new Date(timestamp).getUTCHours();
39
+ isAfter(another: Hour): boolean {
40
+ return this.value > another.value;
41
+ }
66
42
 
67
- return new Hour(hours, formatter);
43
+ isBefore(another: Hour): boolean {
44
+ return this.value < another.value;
68
45
  }
69
46
 
70
- static list(formatter?: HourFormatter) {
71
- return Array.from({ length: 24 }).map((_, index) => new Hour(index, formatter));
47
+ static list(): readonly Hour[] {
48
+ return Array.from({ length: 24 }, (_, index) => new Hour(index));
72
49
  }
73
50
  }
@@ -6,10 +6,15 @@ export class IbanMask {
6
6
  static censor(iban: IBAN): IbanMaskedType {
7
7
  const value = iban.format();
8
8
 
9
- const start = value.slice(0, 4);
10
- const middle = value.slice(5, -5).replace(/\d/g, "*");
9
+ const FIRST_SPACE_INDEX = 4;
10
+ const LAST_SPACE_START_INDEX = value.length - 5;
11
+
12
+ const start = value.slice(0, FIRST_SPACE_INDEX);
13
+ const middle = value.slice(FIRST_SPACE_INDEX + 1, LAST_SPACE_START_INDEX);
11
14
  const end = value.slice(-4);
12
15
 
13
- return `${start} ${middle} ${end}`;
16
+ const maskedMiddle = middle.replace(/[A-Z0-9]/g, "*");
17
+
18
+ return `${start} ${maskedMiddle} ${end}`;
14
19
  }
15
20
  }