@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
package/src/iban.vo.ts CHANGED
@@ -1,24 +1,26 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- // Basic IBAN format regex (2-letter country code + 2 digits + 11–30 alphanumerics)
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)
4
6
  const IBAN_REGEX = /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/;
5
7
 
6
- const IBANValueSchema = z
7
- .string()
8
+ export const IBANValue = z
9
+ .string(IBANError)
8
10
  .trim()
9
11
  .toUpperCase()
10
12
  .transform((val) => val.replace(/\s+/g, ""))
11
- .refine((iban) => IBAN_REGEX.test(iban), { message: "invalid.iban.format" });
12
-
13
- type IBANValueType = z.infer<typeof IBANValueSchema>;
13
+ .refine((iban) => IBAN_REGEX.test(iban), IBANError)
14
+ .brand("IBAN");
14
15
 
15
- type IBANCountryCode = string;
16
+ export type IBANValueType = z.infer<typeof IBANValue>;
17
+ export type IBANCountryCode = string;
16
18
 
17
19
  export class IBAN {
18
20
  private readonly value: IBANValueType;
19
21
 
20
22
  constructor(value: string) {
21
- this.value = IBANValueSchema.parse(value);
23
+ this.value = IBANValue.parse(value);
22
24
  }
23
25
 
24
26
  toString(): IBANValueType {
package/src/image.vo.ts CHANGED
@@ -1,7 +1,22 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const ImageWidth = z.number().int().positive().max(10000).brand("image-width");
4
- export type WidthType = z.infer<typeof ImageWidth>;
3
+ export const ImageWidthError = { error: "invalid.image.width" } as const;
4
+ export const ImageHeightError = { error: "invalid.image.height" } as const;
5
5
 
6
- export const ImageHeight = z.number().int().positive().max(10000).brand("image-height");
7
- export type HeightType = z.infer<typeof ImageHeight>;
6
+ export const ImageWidth = z
7
+ .number(ImageWidthError)
8
+ .int(ImageWidthError)
9
+ .positive(ImageWidthError)
10
+ .max(10_000, ImageWidthError)
11
+ .brand("image-width");
12
+
13
+ export type ImageWidthType = z.infer<typeof ImageWidth>;
14
+
15
+ export const ImageHeight = z
16
+ .number(ImageHeightError)
17
+ .int(ImageHeightError)
18
+ .positive(ImageHeightError)
19
+ .max(10_000, ImageHeightError)
20
+ .brand("image-height");
21
+
22
+ export type ImageHeightType = z.infer<typeof ImageHeight>;
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@ export * from "./clock.vo";
5
5
  export * from "./date-calculator.service";
6
6
  export * from "./date-formatter.service";
7
7
  export * from "./date-range.vo";
8
- export * from "./dates-of-the-week.vo";
9
8
  export * from "./day.vo";
10
9
  export * from "./day-iso-id.vo";
11
10
  export * from "./directory-path-absolute.vo";
@@ -50,7 +49,8 @@ export * from "./rate-limiter.service";
50
49
  export * from "./relative-date.vo";
51
50
  export * from "./reordering.service";
52
51
  export * from "./revision.vo";
53
- export * from "./rounding.service";
52
+ export * from "./rounding.adapter";
53
+ export * from "./rounding.port";
54
54
  export * from "./simple-linear-regression.service";
55
55
  export * from "./size.vo";
56
56
  export * from "./stepper.service";
@@ -1,7 +1,11 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
+ export const LanguageError = { error: "invalid.language" } as const;
4
+
3
5
  export const Language = z
4
- .string()
5
- .length(2)
6
- .regex(/^[a-z]{2}$/, { message: "invalid_language" });
6
+ .string(LanguageError)
7
+ .length(2, LanguageError)
8
+ .regex(/^[a-z]{2}$/, LanguageError)
9
+ .brand("Language");
10
+
7
11
  export type LanguageType = z.infer<typeof Language>;
@@ -1,12 +1,20 @@
1
- import { type RoundingStrategy, RoundToDecimal } from "./rounding.service";
1
+ import { RoundToDecimal } from "./rounding.adapter";
2
+ import type { RoundingPort } from "./rounding.port";
2
3
  import { Sum } from "./sum.service";
3
4
 
5
+ export const MeanEmptyValuesError = "mean.values.empty" as const;
6
+
4
7
  export class Mean {
5
- static calculate(values: number[], rounding: RoundingStrategy = new RoundToDecimal(2)): number {
6
- if (values.length === 0) throw new Error("Values should not be empty");
8
+ private static readonly DEFAULT_ROUNDING: RoundingPort = new RoundToDecimal(2);
9
+
10
+ static calculate(values: number[], rounding?: RoundingPort): number {
11
+ if (values.length === 0) throw new Error(MeanEmptyValuesError);
12
+
13
+ const sum = Sum.of(values);
14
+ const mean = sum / values.length;
7
15
 
8
- const mean = Sum.of(values) / values.length;
16
+ const chosen = rounding ?? Mean.DEFAULT_ROUNDING;
9
17
 
10
- return rounding.round(mean);
18
+ return chosen.round(mean);
11
19
  }
12
20
  }
@@ -1,50 +1,66 @@
1
- import { type RoundingStrategy, RoundToDecimal } from "./rounding.service";
1
+ import { RoundToDecimal } from "./rounding.adapter";
2
+ import type { RoundingPort } from "./rounding.port";
2
3
 
3
4
  type MinMaxScalerValueType = number;
5
+
4
6
  type MinMaxScalerConfigType = {
5
7
  min: MinMaxScalerValueType;
6
8
  max: MinMaxScalerValueType;
7
9
  bound?: { lower: MinMaxScalerValueType; upper: MinMaxScalerValueType };
8
- rounding?: RoundingStrategy;
10
+ rounding?: RoundingPort;
11
+ };
12
+
13
+ export const MinMaxInvalidMinMaxError = "minmax.invalid.minmax" as const;
14
+ export const MinMaxInvalidBoundError = "minmax.invalid.bound" as const;
15
+ export const MinMaxValueOutOfRangeError = "minmax.value.out.of.range" as const;
16
+ export const MinMaxScaledOutOfBoundsError = "minmax.scaled.out.of.bounds" as const;
17
+ export const MinMaxEmptyArrayError = "minmax.empty.array" as const;
18
+
19
+ type ScaleResult = {
20
+ original: MinMaxScalerValueType;
21
+ scaled: MinMaxScalerValueType;
22
+ isMin: boolean;
23
+ isMax: boolean;
24
+ };
25
+
26
+ type DescaleResult = {
27
+ original: MinMaxScalerValueType;
28
+ scaled: MinMaxScalerValueType;
29
+ isLowerBound: boolean;
30
+ isUpperBound: boolean;
9
31
  };
10
32
 
11
33
  export class MinMaxScaler {
34
+ private static readonly DEFAULT_ROUNDING: RoundingPort = new RoundToDecimal(2);
35
+
12
36
  private readonly min: MinMaxScalerValueType;
13
37
  private readonly max: MinMaxScalerValueType;
14
38
  private readonly lower: MinMaxScalerValueType;
15
39
  private readonly upper: MinMaxScalerValueType;
16
-
17
- private readonly rounding: RoundingStrategy;
40
+ private readonly rounding: RoundingPort;
18
41
 
19
42
  constructor(config: MinMaxScalerConfigType) {
20
- const rounding = config.rounding ?? new RoundToDecimal(2);
21
-
22
43
  const lower = config.bound?.lower ?? 0;
23
44
  const upper = config.bound?.upper ?? 1;
24
45
 
25
- if (config.max - config.min < 0) throw new Error("Invalid MinMaxScaler min-max config");
26
- if (upper - lower <= 0) throw new Error("Invalid MinMaxScaler bound config");
27
-
28
- this.rounding = rounding;
46
+ if (config.max - config.min < 0) throw new Error(MinMaxInvalidMinMaxError);
47
+ if (upper - lower <= 0) throw new Error(MinMaxInvalidBoundError);
29
48
 
49
+ this.rounding = config.rounding ?? MinMaxScaler.DEFAULT_ROUNDING;
30
50
  this.min = config.min;
31
51
  this.max = config.max;
32
52
  this.lower = lower;
33
53
  this.upper = upper;
34
54
  }
35
55
 
36
- scale(value: MinMaxScalerValueType) {
56
+ scale(value: MinMaxScalerValueType): ScaleResult {
37
57
  const { min, max, lower, upper } = this;
38
58
 
39
- if (value < min || value > max) throw new Error("Value out of min/max range");
59
+ if (value < min || value > max) throw new Error(MinMaxValueOutOfRangeError);
40
60
 
41
- if (min === max)
42
- return {
43
- original: value,
44
- scaled: (lower + upper) / 2,
45
- isMin: value === min,
46
- isMax: value === max,
47
- };
61
+ if (min === max) {
62
+ return { original: value, scaled: (lower + upper) / 2, isMin: value === min, isMax: value === max };
63
+ }
48
64
 
49
65
  const result = ((value - min) / (max - min)) * (upper - lower) + lower;
50
66
 
@@ -56,10 +72,10 @@ export class MinMaxScaler {
56
72
  };
57
73
  }
58
74
 
59
- descale(scaled: MinMaxScalerValueType) {
75
+ descale(scaled: MinMaxScalerValueType): DescaleResult {
60
76
  const { min, max, lower, upper } = this;
61
77
 
62
- if (scaled < lower || scaled > upper) throw new Error("Scaled value out of bounds");
78
+ if (scaled < lower || scaled > upper) throw new Error(MinMaxScaledOutOfBoundsError);
63
79
 
64
80
  const result = ((scaled - lower) / (upper - lower)) * (max - min) + min;
65
81
 
@@ -71,9 +87,8 @@ export class MinMaxScaler {
71
87
  };
72
88
  }
73
89
 
74
- static getMinMax(values: MinMaxScalerValueType[]) {
75
- if (values.length === 0) throw new Error("An empty array supplied");
76
-
90
+ static getMinMax(values: MinMaxScalerValueType[]): { min: number; max: number } {
91
+ if (values.length === 0) throw new Error(MinMaxEmptyArrayError);
77
92
  return { min: Math.min(...values), max: Math.max(...values) };
78
93
  }
79
94
  }
package/src/minute.vo.ts CHANGED
@@ -1,42 +1,45 @@
1
1
  import type { TimestampType } from "./timestamp.vo";
2
2
 
3
+ export const MinuteValueError = "invalid.minute" as const;
4
+
3
5
  export class Minute {
4
6
  private readonly value: number;
5
7
 
6
8
  static readonly ZERO = new Minute(0);
7
-
8
9
  static readonly MAX = new Minute(59);
9
10
 
10
11
  constructor(candidate: number) {
11
- if (!Number.isInteger(candidate)) throw new Error("Invalid minute");
12
- if (candidate < 0) throw new Error("Invalid minute");
13
- if (candidate >= 60) throw new Error("Invalid minute");
14
-
12
+ if (!Number.isInteger(candidate) || candidate < 0 || candidate >= 60) {
13
+ throw new Error(MinuteValueError);
14
+ }
15
15
  this.value = candidate;
16
16
  }
17
17
 
18
- static fromUtcTimestamp(timestamp: TimestampType): Minute {
19
- const minutes = new Date(timestamp).getUTCMinutes();
20
- return new Minute(minutes);
18
+ static fromEpochMs(timestamp: TimestampType): Minute {
19
+ return new Minute(new Date(timestamp).getUTCMinutes());
20
+ }
21
+
22
+ get(): number {
23
+ return this.value;
21
24
  }
22
25
 
23
- get() {
24
- return { raw: this.value, formatted: this.value.toString().padStart(2, "0") };
26
+ toString(): string {
27
+ return this.value.toString().padStart(2, "0");
25
28
  }
26
29
 
27
30
  equals(another: Minute): boolean {
28
- return this.value === another.get().raw;
31
+ return this.value === another.value;
29
32
  }
30
33
 
31
34
  isAfter(another: Minute): boolean {
32
- return this.value > another.get().raw;
35
+ return this.value > another.value;
33
36
  }
34
37
 
35
38
  isBefore(another: Minute): boolean {
36
- return this.value < another.get().raw;
39
+ return this.value < another.value;
37
40
  }
38
41
 
39
- static list() {
40
- return Array.from({ length: 60 }).map((_, index) => new Minute(index));
42
+ static list(): readonly Minute[] {
43
+ return Array.from({ length: 60 }, (_, index) => new Minute(index));
41
44
  }
42
45
  }
package/src/money.vo.ts CHANGED
@@ -1,66 +1,72 @@
1
1
  import { z } from "zod/v4";
2
- import { type RoundingStrategy, RoundToNearest } from "./rounding.service";
2
+ import { RoundToNearest } from "./rounding.adapter";
3
+ import type { RoundingPort } from "./rounding.port";
4
+
5
+ export const MoneyAmountInvalidError = { error: "money.amount.invalid" } as const;
6
+ export const MoneyMultiplicationFactorInvalidError = {
7
+ error: "money.multiplication-factor.invalid",
8
+ } as const;
9
+ export const MoneyDivisionFactorInvalidError = { error: "money.division-factor.invalid" } as const;
10
+ export const MoneySubtractLessThanZeroError = "money.subtract.less.than.zero" as const;
3
11
 
4
12
  export const MoneyAmount = z
5
- .number()
6
- .int({ message: "money.amount.invalid" })
7
- .min(0, { message: "money.amount.invalid" })
13
+ .number(MoneyAmountInvalidError)
14
+ .int(MoneyAmountInvalidError)
15
+ .min(0, MoneyAmountInvalidError)
8
16
  .brand("MoneyAmount");
9
17
 
10
18
  export type MoneyAmountType = z.infer<typeof MoneyAmount>;
11
19
 
12
20
  export const MoneyMultiplicationFactor = z
13
- .number()
14
- .min(0, { message: "money.multiplication-factor.invalid" })
21
+ .number(MoneyMultiplicationFactorInvalidError)
22
+ .min(0, MoneyMultiplicationFactorInvalidError)
15
23
  .brand("MoneyMultiplicationFactor");
16
24
 
17
25
  export type MoneyMultiplicationFactorType = z.infer<typeof MoneyMultiplicationFactor>;
18
26
 
19
27
  export const MoneyDivisionFactor = z
20
- .number()
21
- .min(0, { message: "money.division-factor.invalid" })
22
- .refine((value) => value !== 0, { message: "money.division-factor.invalid" })
28
+ .number(MoneyDivisionFactorInvalidError)
29
+ .gt(0, MoneyDivisionFactorInvalidError)
23
30
  .brand("MoneyDivisionFactor");
24
31
 
25
32
  export type MoneyDivisionFactorType = z.infer<typeof MoneyDivisionFactor>;
26
33
 
27
34
  export class Money {
28
35
  private static readonly ZERO = 0;
36
+ private static readonly DEFAULT_ROUNDING: RoundingPort = new RoundToNearest();
29
37
 
30
38
  private readonly amount: MoneyAmountType;
39
+ private readonly rounding: RoundingPort;
31
40
 
32
- private readonly rounding: RoundingStrategy;
33
-
34
- constructor(value: number = Money.ZERO, rounding?: RoundingStrategy) {
41
+ constructor(value: number = Money.ZERO, rounding?: RoundingPort) {
35
42
  this.amount = MoneyAmount.parse(value);
36
- this.rounding = rounding ?? new RoundToNearest();
43
+ this.rounding = rounding ?? Money.DEFAULT_ROUNDING;
37
44
  }
38
45
 
39
46
  getAmount(): MoneyAmountType {
40
47
  return this.amount;
41
48
  }
42
49
 
43
- add(money: Money) {
50
+ add(money: Money): Money {
44
51
  const result = this.rounding.round(this.amount + money.getAmount());
45
52
 
46
53
  return new Money(MoneyAmount.parse(result), this.rounding);
47
54
  }
48
55
 
49
- multiply(factor: MoneyMultiplicationFactorType) {
56
+ multiply(factor: MoneyMultiplicationFactorType): Money {
50
57
  const result = this.rounding.round(this.amount * factor);
51
58
 
52
59
  return new Money(MoneyAmount.parse(result), this.rounding);
53
60
  }
54
61
 
55
- subtract(money: Money) {
62
+ subtract(money: Money): Money {
56
63
  const result = this.rounding.round(this.amount - money.getAmount());
57
64
 
58
- if (result < Money.ZERO) throw new Error("Less than zero");
59
-
65
+ if (result < Money.ZERO) throw new Error(MoneySubtractLessThanZeroError);
60
66
  return new Money(MoneyAmount.parse(result), this.rounding);
61
67
  }
62
68
 
63
- divide(factor: MoneyDivisionFactorType) {
69
+ divide(factor: MoneyDivisionFactorType): Money {
64
70
  const result = this.rounding.round(this.amount / factor);
65
71
 
66
72
  return new Money(MoneyAmount.parse(result), this.rounding);
@@ -83,10 +89,7 @@ export class Money {
83
89
  }
84
90
 
85
91
  format(): string {
86
- const result = this.amount / 100;
87
-
88
- const whole = Math.floor(result);
89
-
92
+ const whole = Math.floor(this.amount / 100);
90
93
  const fraction = this.amount % 100;
91
94
  const fractionFormatted = fraction.toString().padStart(2, "0");
92
95
 
@@ -1,18 +1,20 @@
1
1
  import { ZScore } from "./z-score.service";
2
2
 
3
+ export const OutlierDetectorMinValuesError = "outlier.detector.min.values" as const;
4
+
3
5
  export class OutlierDetector {
4
6
  private readonly zScore: ZScore;
5
-
6
7
  private readonly threshold: number;
7
8
 
8
9
  constructor(values: number[], threshold: number) {
9
- if (values.length < 2) throw new Error("At least two values are needed");
10
+ if (values.length < 2) throw new Error(OutlierDetectorMinValuesError);
10
11
 
11
12
  this.zScore = new ZScore(values);
12
13
  this.threshold = Math.abs(threshold);
13
14
  }
14
15
 
15
- check(value: number): boolean {
16
- return this.zScore.calculate(value) <= this.threshold;
16
+ isInlier(value: number): boolean {
17
+ const score = this.zScore.calculate(value);
18
+ return Math.abs(score) <= this.threshold;
17
19
  }
18
20
  }
@@ -1,12 +1,11 @@
1
- import { type RoundingStrategy, RoundToNearest } from "./rounding.service";
1
+ import { RoundToNearest } from "./rounding.adapter";
2
+ import type { RoundingPort } from "./rounding.port";
3
+
4
+ export const PercentageInvalidDenominatorError = "percentage.invalid.denominator" as const;
2
5
 
3
6
  export class Percentage {
4
- static of(
5
- numerator: number,
6
- denominator: number,
7
- rounding: RoundingStrategy = new RoundToNearest(),
8
- ): number {
9
- if (denominator === 0) throw new Error("Invalid denominator");
7
+ static of(numerator: number, denominator: number, rounding: RoundingPort = new RoundToNearest()): number {
8
+ if (denominator === 0) throw new Error(PercentageInvalidDenominatorError);
10
9
  if (numerator === 0) return 0;
11
10
  return rounding.round((numerator / denominator) * 100);
12
11
  }
@@ -1,18 +1,21 @@
1
1
  import { Mean } from "./mean.service";
2
- import { type RoundingStrategy, RoundToDecimal } from "./rounding.service";
2
+ import { RoundToDecimal } from "./rounding.adapter";
3
+ import type { RoundingPort } from "./rounding.port";
3
4
  import { Sum } from "./sum.service";
4
5
 
6
+ export const PopulationStandardDeviationMinValuesError = "population.standard.deviation.min.values" as const;
7
+
5
8
  export class PopulationStandardDeviation {
6
- static calculate(values: number[], rounding: RoundingStrategy = new RoundToDecimal(2)): number {
7
- if (values.length < 2) throw new Error("At least two values are needed");
9
+ static calculate(values: number[], rounding: RoundingPort = new RoundToDecimal(2)): number {
10
+ if (values.length < 2) throw new Error(PopulationStandardDeviationMinValuesError);
8
11
 
9
12
  const mean = Mean.calculate(values);
10
- const n = values.length;
13
+ const count = values.length;
11
14
 
12
15
  const squaredDifferences = values.map((value) => (value - mean) ** 2);
13
16
  const sumOfSquaredDifferences = Sum.of(squaredDifferences);
14
17
 
15
- const variance = sumOfSquaredDifferences / n;
18
+ const variance = sumOfSquaredDifferences / count;
16
19
 
17
20
  return rounding.round(Math.sqrt(variance));
18
21
  }
@@ -1,14 +1,22 @@
1
1
  type RandomGenerateConfigType = { min: number; max: number };
2
2
 
3
+ export const RandomMinNotIntegerError = "random.min.not.integer" as const;
4
+ export const RandomMaxNotIntegerError = "random.max.not.integer" as const;
5
+ export const RandomMinEqualsMaxError = "random.min.equals.max" as const;
6
+ export const RandomMinGreaterThanMaxError = "random.min.greater.than.max" as const;
7
+
3
8
  export class Random {
4
- static generate(config?: RandomGenerateConfigType) {
5
- const min = config?.min ?? 0;
6
- const max = config?.max ?? 1;
7
-
8
- if (!Number.isInteger(min)) throw new Error("Minimum value is not an integer");
9
- if (!Number.isInteger(max)) throw new Error("Maximum value is not an integer");
10
- if (min === max) throw new Error("Minimum and maximum values cannot be equal");
11
- if (min > max) throw new Error("Minimum value cannot be greater than maximum value");
9
+ private static readonly DEFAULT_MIN = 0;
10
+ private static readonly DEFAULT_MAX = 1;
11
+
12
+ static generate(config?: RandomGenerateConfigType): number {
13
+ const min = config ? config.min : Random.DEFAULT_MIN;
14
+ const max = config ? config.max : Random.DEFAULT_MAX;
15
+
16
+ if (!Number.isInteger(min)) throw new Error(RandomMinNotIntegerError);
17
+ if (!Number.isInteger(max)) throw new Error(RandomMaxNotIntegerError);
18
+ if (min === max) throw new Error(RandomMinEqualsMaxError);
19
+ if (min > max) throw new Error(RandomMinGreaterThanMaxError);
12
20
 
13
21
  return Math.floor(Math.random() * (max - min + 1)) + min;
14
22
  }
@@ -11,7 +11,6 @@ export class RelativeDate {
11
11
 
12
12
  static falsy(timestampMs: Falsy<TimestampType>): RelativeDateType | null {
13
13
  if (!timestampMs) return null;
14
-
15
14
  return RelativeDate._format(timestampMs);
16
15
  }
17
16
 
@@ -0,0 +1,33 @@
1
+ import type { RoundingPort } from "./rounding.port";
2
+
3
+ export class RoundToNearest implements RoundingPort {
4
+ round(value: number): number {
5
+ return Math.round(value);
6
+ }
7
+ }
8
+
9
+ export class RoundUp implements RoundingPort {
10
+ round(value: number): number {
11
+ return Math.ceil(value);
12
+ }
13
+ }
14
+
15
+ export class RoundDown implements RoundingPort {
16
+ round(value: number): number {
17
+ return Math.floor(value);
18
+ }
19
+ }
20
+
21
+ export const RoundingDecimalsError = "invalid.rounding.decimals" as const;
22
+
23
+ export class RoundToDecimal implements RoundingPort {
24
+ constructor(private readonly decimals: number) {
25
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 100) {
26
+ throw new Error(RoundingDecimalsError);
27
+ }
28
+ }
29
+
30
+ round(value: number): number {
31
+ return Number.parseFloat(value.toFixed(this.decimals));
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ export interface RoundingPort {
2
+ round(value: number): number;
3
+ }
@@ -1,56 +1,66 @@
1
- import { type RoundingStrategy, RoundToNearest } from "./rounding.service";
2
- import { Sum } from "./sum.service";
1
+ import { RoundToNearest } from "./rounding.adapter";
2
+ import type { RoundingPort } from "./rounding.port";
3
3
 
4
4
  export type SLRPairType = { x: number; y: number };
5
5
  export type SLRParamsType = { a: number; b: number };
6
6
  export type SLRPredictionType = number;
7
7
 
8
+ export const SLRMinPairsError = "slr.min.pairs" as const;
9
+ export const SLRSumXTooBigError = "slr.sum.x.too.big" as const;
10
+ export const SLRSumYTooBigError = "slr.sum.y.too.big" as const;
11
+ export const SLRSumXSquaredTooBigError = "slr.sum.x.squared.too.big" as const;
12
+ export const SLRSumXTimesYTooBigError = "slr.sum.x.times.y.too.big" as const;
13
+ export const SLRModelCreationError = "slr.model.creation" as const;
14
+
8
15
  export class SimpleLinearRegression {
9
- private readonly rounding: RoundingStrategy = new RoundToNearest();
16
+ private static readonly DEFAULT_ROUNDING: RoundingPort = new RoundToNearest();
10
17
 
11
- constructor(
12
- private readonly params: SLRParamsType,
13
- rounding?: RoundingStrategy,
14
- ) {
15
- this.rounding = rounding ?? this.rounding;
16
- }
18
+ private readonly params: SLRParamsType;
19
+ private readonly rounding: RoundingPort;
17
20
 
18
- static fromPairs(pairs: SLRPairType[], rounding?: RoundingStrategy) {
19
- const n = pairs.length;
21
+ constructor(params: SLRParamsType, rounding?: RoundingPort) {
22
+ this.params = params;
23
+ this.rounding = rounding ?? SimpleLinearRegression.DEFAULT_ROUNDING;
24
+ }
20
25
 
21
- if (n < 2) throw new Error("At least two pairs needed");
26
+ static fromPairs(pairs: SLRPairType[], rounding?: RoundingPort): SimpleLinearRegression {
27
+ const count = pairs.length;
22
28
 
23
- const x = pairs.map((pair) => pair.x);
24
- const y = pairs.map((pair) => pair.y);
25
- const xx = x.map((x) => x ** 2);
26
- const xy = pairs.map((pair) => pair.x * pair.y);
29
+ if (count < 2) throw new Error(SLRMinPairsError);
27
30
 
28
- const sX = Sum.of(x);
29
- if (sX >= Number.MAX_SAFE_INTEGER) throw new Error("Sum of x values is too big");
31
+ let sumX = 0;
32
+ let sumY = 0;
33
+ let sumXX = 0;
34
+ let sumXY = 0;
30
35
 
31
- const sY = Sum.of(y);
32
- if (sY >= Number.MAX_SAFE_INTEGER) throw new Error("Sum of y values is too big");
36
+ for (let index = 0; index < count; index++) {
37
+ const pair = pairs[index];
38
+ sumX += pair.x;
39
+ sumY += pair.y;
40
+ sumXX += pair.x * pair.x;
41
+ sumXY += pair.x * pair.y;
42
+ }
33
43
 
34
- const sSX = Sum.of(xx);
35
- if (sSX >= Number.MAX_SAFE_INTEGER) throw new Error("Sum of x squared values is too big");
44
+ if (Math.abs(sumX) >= Number.MAX_SAFE_INTEGER) throw new Error(SLRSumXTooBigError);
45
+ if (Math.abs(sumY) >= Number.MAX_SAFE_INTEGER) throw new Error(SLRSumYTooBigError);
46
+ if (Math.abs(sumXY) >= Number.MAX_SAFE_INTEGER) throw new Error(SLRSumXTimesYTooBigError);
47
+ if (Math.abs(sumXX) >= Number.MAX_SAFE_INTEGER) throw new Error(SLRSumXSquaredTooBigError);
36
48
 
37
- const sXY = Sum.of(xy);
38
- if (sXY >= Number.MAX_SAFE_INTEGER) throw new Error("Sum of x times y values is too big");
49
+ const bDenominator = sumXX - sumX ** 2 / count;
39
50
 
40
- const bDenominator = sSX - sX ** 2 / n;
41
- if (bDenominator === 0) throw new Error("Unable to create the model");
51
+ if (bDenominator === 0) throw new Error(SLRModelCreationError);
42
52
 
43
- const b = (sXY - (sX * sY) / n) / bDenominator;
44
- const a = (sY - b * sX) / n;
53
+ const b = (sumXY - (sumX * sumY) / count) / bDenominator;
54
+ const a = (sumY - b * sumX) / count;
45
55
 
46
56
  return new SimpleLinearRegression({ a, b }, rounding);
47
57
  }
48
58
 
49
- predict(x: SLRPairType["x"], strategy?: RoundingStrategy): SLRPredictionType {
50
- const rounding = strategy ?? this.rounding;
59
+ predict(x: SLRPairType["x"], rounding?: RoundingPort): SLRPredictionType {
60
+ const chosen = rounding ?? this.rounding;
51
61
  const prediction = this.params.b * x + this.params.a;
52
62
 
53
- return rounding.round(prediction);
63
+ return chosen.round(prediction);
54
64
  }
55
65
 
56
66
  inspect(): SimpleLinearRegression["params"] {
package/src/size.vo.ts 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
 
4
4
  export enum SizeUnit {
5
5
  b = "b",