@bgord/tools 0.13.3 → 0.14.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.
@@ -0,0 +1,34 @@
1
+ import { type RoundingStrategy } from "./rounding.service";
2
+ export declare enum HeightUnit {
3
+ cm = "cm",
4
+ ft_in = "ft_in"
5
+ }
6
+ export declare class Height {
7
+ private readonly millimeters;
8
+ private static readonly MILLIMETERS_PER_CENTIMETER;
9
+ private static readonly MILLIMETERS_PER_INCH;
10
+ private static readonly INCHES_PER_FOOT;
11
+ private constructor();
12
+ static fromCentimeters(centimeters: number, rounding?: RoundingStrategy): Height;
13
+ static fromFeetInches(feet: number, inches?: number, rounding?: RoundingStrategy): Height;
14
+ static fromMillimeters(millimeters: number, rounding?: RoundingStrategy): Height;
15
+ static zero(): Height;
16
+ toMillimeters(): number;
17
+ toCentimeters(rounding?: RoundingStrategy): number;
18
+ toFeetInches(rounding?: RoundingStrategy): {
19
+ feet: number;
20
+ inches: number;
21
+ };
22
+ format(unit: HeightUnit, roundingStrategy?: RoundingStrategy): string;
23
+ equals(other: Height): boolean;
24
+ compare(other: Height): -1 | 0 | 1;
25
+ greaterThan(other: Height): boolean;
26
+ lessThan(other: Height): boolean;
27
+ isZero(): boolean;
28
+ toJSON(): {
29
+ mm: number;
30
+ };
31
+ static fromJSON(input: {
32
+ mm: number;
33
+ }): Height;
34
+ }
@@ -0,0 +1,100 @@
1
+ import { z } from "zod/v4";
2
+ import { RoundToDecimal, RoundToNearest } from "./rounding.service";
3
+ const FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
4
+ const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
5
+ const NonNegativeIntegerMillimeters = FiniteNumericValue.int().min(0, {
6
+ message: "Millimeters must be an integer greater than or equal to 0",
7
+ });
8
+ const NonNegativeIntegerValue = FiniteNumericValue.int().min(0, {
9
+ message: "Value must be an integer greater than or equal to 0",
10
+ });
11
+ export var HeightUnit;
12
+ (function (HeightUnit) {
13
+ HeightUnit["cm"] = "cm";
14
+ HeightUnit["ft_in"] = "ft_in";
15
+ })(HeightUnit || (HeightUnit = {}));
16
+ export class Height {
17
+ millimeters;
18
+ static MILLIMETERS_PER_CENTIMETER = 10;
19
+ static MILLIMETERS_PER_INCH = 25.4;
20
+ static INCHES_PER_FOOT = 12;
21
+ constructor(millimeters) {
22
+ this.millimeters = millimeters;
23
+ }
24
+ static fromCentimeters(centimeters, rounding = new RoundToNearest()) {
25
+ NonNegativeNumericValue.parse(centimeters);
26
+ const mmFloat = centimeters * Height.MILLIMETERS_PER_CENTIMETER;
27
+ const mmRounded = rounding.round(mmFloat);
28
+ NonNegativeIntegerMillimeters.parse(mmRounded);
29
+ return new Height(mmRounded);
30
+ }
31
+ static fromFeetInches(feet, inches = 0, rounding = new RoundToNearest()) {
32
+ NonNegativeNumericValue.parse(feet);
33
+ NonNegativeNumericValue.parse(inches);
34
+ const totalInches = feet * Height.INCHES_PER_FOOT + inches;
35
+ const mmFloat = totalInches * Height.MILLIMETERS_PER_INCH;
36
+ const mmRounded = rounding.round(mmFloat);
37
+ NonNegativeIntegerMillimeters.parse(mmRounded);
38
+ return new Height(mmRounded);
39
+ }
40
+ static fromMillimeters(millimeters, rounding = new RoundToNearest()) {
41
+ NonNegativeNumericValue.parse(millimeters);
42
+ const mmRounded = rounding.round(millimeters);
43
+ NonNegativeIntegerMillimeters.parse(mmRounded);
44
+ return new Height(mmRounded);
45
+ }
46
+ static zero() {
47
+ return new Height(0);
48
+ }
49
+ toMillimeters() {
50
+ return this.millimeters;
51
+ }
52
+ toCentimeters(rounding) {
53
+ const cm = this.millimeters / Height.MILLIMETERS_PER_CENTIMETER;
54
+ return rounding ? rounding.round(cm) : cm;
55
+ }
56
+ toFeetInches(rounding = new RoundToNearest()) {
57
+ const totalInchesFloat = this.millimeters / Height.MILLIMETERS_PER_INCH;
58
+ const totalInchesRounded = rounding.round(totalInchesFloat);
59
+ const integerInches = NonNegativeIntegerValue.parse(totalInchesRounded);
60
+ const feet = (integerInches - (integerInches % Height.INCHES_PER_FOOT)) / Height.INCHES_PER_FOOT;
61
+ const inches = integerInches % Height.INCHES_PER_FOOT;
62
+ return { feet, inches };
63
+ }
64
+ format(unit, roundingStrategy) {
65
+ return {
66
+ [HeightUnit.cm]: () => {
67
+ const rounding = roundingStrategy ?? new RoundToDecimal(1);
68
+ return `${this.toCentimeters(rounding)} cm`;
69
+ },
70
+ [HeightUnit.ft_in]: () => {
71
+ const rounding = roundingStrategy ?? new RoundToNearest();
72
+ const { feet, inches } = this.toFeetInches(rounding);
73
+ return `${feet}′${inches}″`;
74
+ },
75
+ }[unit]();
76
+ }
77
+ equals(other) {
78
+ return this.millimeters === other.millimeters;
79
+ }
80
+ compare(other) {
81
+ if (this.equals(other))
82
+ return 0;
83
+ return this.millimeters < other.millimeters ? -1 : 1;
84
+ }
85
+ greaterThan(other) {
86
+ return this.millimeters > other.millimeters;
87
+ }
88
+ lessThan(other) {
89
+ return this.millimeters < other.millimeters;
90
+ }
91
+ isZero() {
92
+ return this.millimeters === 0;
93
+ }
94
+ toJSON() {
95
+ return { mm: this.millimeters };
96
+ }
97
+ static fromJSON(input) {
98
+ return Height.fromMillimeters(input.mm);
99
+ }
100
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod/v4";
2
- export declare const Width: z.core.$ZodBranded<z.ZodNumber, "Width">;
3
- export type WidthType = z.infer<typeof Width>;
4
- export declare const Height: z.core.$ZodBranded<z.ZodNumber, "Height">;
5
- export type HeightType = z.infer<typeof Height>;
2
+ export declare const ImageWidth: z.core.$ZodBranded<z.ZodNumber, "image-width">;
3
+ export type WidthType = z.infer<typeof ImageWidth>;
4
+ export declare const ImageHeight: z.core.$ZodBranded<z.ZodNumber, "image-height">;
5
+ export type HeightType = z.infer<typeof ImageHeight>;
package/dist/image.vo.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { z } from "zod/v4";
2
- export const Width = z.number().int().positive().max(10000).brand("Width");
3
- export const Height = z.number().int().positive().max(10000).brand("Height");
2
+ export const ImageWidth = z.number().int().positive().max(10000).brand("image-width");
3
+ export const ImageHeight = z.number().int().positive().max(10000).brand("image-height");
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export * from "./file-path-relative-schema.vo";
20
20
  export * from "./filename.vo";
21
21
  export * from "./filename-from-string.vo";
22
22
  export * from "./filename-suffix.vo";
23
+ export * from "./height.vo";
23
24
  export * from "./hour.vo";
24
25
  export * from "./iban.vo";
25
26
  export * from "./iban-mask.service";
@@ -65,6 +66,7 @@ export * from "./visually-unambiguous-characters-generator.service";
65
66
  export * from "./week.vo";
66
67
  export * from "./week-iso-id.vo";
67
68
  export * from "./weekday.vo";
69
+ export * from "./weight.vo";
68
70
  export * from "./year.vo";
69
71
  export * from "./year-iso-id.vo";
70
72
  export * from "./z-score.service";
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ export * from "./file-path-relative-schema.vo";
20
20
  export * from "./filename.vo";
21
21
  export * from "./filename-from-string.vo";
22
22
  export * from "./filename-suffix.vo";
23
+ export * from "./height.vo";
23
24
  export * from "./hour.vo";
24
25
  export * from "./iban.vo";
25
26
  export * from "./iban-mask.service";
@@ -65,6 +66,7 @@ export * from "./visually-unambiguous-characters-generator.service";
65
66
  export * from "./week.vo";
66
67
  export * from "./week-iso-id.vo";
67
68
  export * from "./weekday.vo";
69
+ export * from "./weight.vo";
68
70
  export * from "./year.vo";
69
71
  export * from "./year-iso-id.vo";
70
72
  export * from "./z-score.service";
@@ -1 +1 @@
1
- {"root":["../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/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/year-iso-id.vo.ts","../src/year.vo.ts","../src/z-score.service.ts"],"version":"5.9.2"}
1
+ {"root":["../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"}
@@ -0,0 +1,38 @@
1
+ import { type RoundingStrategy } from "./rounding.service";
2
+ export declare enum WeightUnit {
3
+ kg = "kg",
4
+ lb = "lb"
5
+ }
6
+ export declare class Weight {
7
+ private readonly grams;
8
+ private static readonly GRAMS_PER_KILOGRAM;
9
+ private static readonly POUNDS_PER_KILOGRAM;
10
+ private static readonly KILOGRAMS_PER_POUND;
11
+ private constructor();
12
+ static fromKilograms(kilograms: number): Weight;
13
+ static fromPounds(pounds: number): Weight;
14
+ static fromGrams(grams: number): Weight;
15
+ static zero(): Weight;
16
+ toGrams(): number;
17
+ toKilograms(rounding?: RoundingStrategy): number;
18
+ toPounds(rounding?: RoundingStrategy): number;
19
+ format(unit: WeightUnit, rounding?: RoundingStrategy): string;
20
+ add(other: Weight): Weight;
21
+ subtract(other: Weight): Weight;
22
+ multiply(factor: number): Weight;
23
+ divideByScalar(divisor: number): Weight;
24
+ equals(other: Weight): boolean;
25
+ compare(other: Weight): -1 | 0 | 1;
26
+ greaterThan(other: Weight): boolean;
27
+ greaterThanOrEqual(other: Weight): boolean;
28
+ lessThan(other: Weight): boolean;
29
+ lessThanOrEqual(other: Weight): boolean;
30
+ isZero(): boolean;
31
+ isPositive(): boolean;
32
+ toJSON(): {
33
+ g: number;
34
+ };
35
+ static fromJSON(input: {
36
+ g: number;
37
+ }): Weight;
38
+ }
@@ -0,0 +1,109 @@
1
+ import { z } from "zod/v4";
2
+ import { RoundToDecimal } from "./rounding.service";
3
+ const FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
4
+ const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
5
+ const PositiveNumericValue = FiniteNumericValue.gt(0, { message: "Must be greater than 0" });
6
+ const NonNegativeIntegerGrams = FiniteNumericValue.int().min(0, {
7
+ message: "Grams must be an integer greater than or equal to 0",
8
+ });
9
+ export var WeightUnit;
10
+ (function (WeightUnit) {
11
+ WeightUnit["kg"] = "kg";
12
+ WeightUnit["lb"] = "lb";
13
+ })(WeightUnit || (WeightUnit = {}));
14
+ export class Weight {
15
+ grams;
16
+ static GRAMS_PER_KILOGRAM = 1_000;
17
+ static POUNDS_PER_KILOGRAM = 2.2046226218487757;
18
+ static KILOGRAMS_PER_POUND = 1 / Weight.POUNDS_PER_KILOGRAM;
19
+ constructor(grams) {
20
+ this.grams = grams;
21
+ }
22
+ static fromKilograms(kilograms) {
23
+ NonNegativeNumericValue.parse(kilograms);
24
+ const gramsRounded = Math.round(kilograms * Weight.GRAMS_PER_KILOGRAM);
25
+ NonNegativeIntegerGrams.parse(gramsRounded);
26
+ return new Weight(gramsRounded);
27
+ }
28
+ static fromPounds(pounds) {
29
+ NonNegativeNumericValue.parse(pounds);
30
+ const gramsRounded = Math.round(pounds * Weight.KILOGRAMS_PER_POUND * Weight.GRAMS_PER_KILOGRAM);
31
+ NonNegativeIntegerGrams.parse(gramsRounded);
32
+ return new Weight(gramsRounded);
33
+ }
34
+ static fromGrams(grams) {
35
+ NonNegativeNumericValue.parse(grams);
36
+ const gramsRounded = Math.round(grams);
37
+ NonNegativeIntegerGrams.parse(gramsRounded);
38
+ return new Weight(gramsRounded);
39
+ }
40
+ static zero() {
41
+ return new Weight(0);
42
+ }
43
+ toGrams() {
44
+ return this.grams;
45
+ }
46
+ toKilograms(rounding) {
47
+ const kilograms = this.grams / Weight.GRAMS_PER_KILOGRAM;
48
+ return rounding ? rounding.round(kilograms) : kilograms;
49
+ }
50
+ toPounds(rounding) {
51
+ const pounds = (this.grams / Weight.GRAMS_PER_KILOGRAM) * Weight.POUNDS_PER_KILOGRAM;
52
+ return rounding ? rounding.round(pounds) : pounds;
53
+ }
54
+ format(unit, rounding = new RoundToDecimal(2)) {
55
+ const value = { [WeightUnit.kg]: this.toKilograms(rounding), [WeightUnit.lb]: this.toPounds(rounding) }[unit];
56
+ return `${value.toString()} ${unit}`;
57
+ }
58
+ add(other) {
59
+ return new Weight(this.grams + other.grams);
60
+ }
61
+ subtract(other) {
62
+ const result = this.grams - other.grams;
63
+ return new Weight(result < 0 ? 0 : result);
64
+ }
65
+ multiply(factor) {
66
+ NonNegativeNumericValue.parse(factor);
67
+ const gramsRounded = Math.round(this.grams * factor);
68
+ NonNegativeIntegerGrams.parse(gramsRounded);
69
+ return new Weight(gramsRounded);
70
+ }
71
+ divideByScalar(divisor) {
72
+ PositiveNumericValue.parse(divisor);
73
+ const gramsRounded = Math.round(this.grams / divisor);
74
+ NonNegativeIntegerGrams.parse(gramsRounded);
75
+ return new Weight(gramsRounded);
76
+ }
77
+ equals(other) {
78
+ return this.grams === other.grams;
79
+ }
80
+ compare(other) {
81
+ if (this.grams === other.grams)
82
+ return 0;
83
+ return this.grams < other.grams ? -1 : 1;
84
+ }
85
+ greaterThan(other) {
86
+ return this.grams > other.grams;
87
+ }
88
+ greaterThanOrEqual(other) {
89
+ return this.grams >= other.grams;
90
+ }
91
+ lessThan(other) {
92
+ return this.grams < other.grams;
93
+ }
94
+ lessThanOrEqual(other) {
95
+ return this.grams <= other.grams;
96
+ }
97
+ isZero() {
98
+ return this.grams === 0;
99
+ }
100
+ isPositive() {
101
+ return this.grams > 0;
102
+ }
103
+ toJSON() {
104
+ return { g: this.grams };
105
+ }
106
+ static fromJSON(input) {
107
+ return Weight.fromGrams(input.g);
108
+ }
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgord/tools",
3
- "version": "0.13.3",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Bartosz Gordon",
@@ -24,11 +24,11 @@
24
24
  "@biomejs/biome": "2.2.4",
25
25
  "@commitlint/cli": "20.0.0",
26
26
  "@commitlint/config-conventional": "20.0.0",
27
- "@types/bun": "1.2.22",
27
+ "@types/bun": "1.2.23",
28
28
  "@types/mime-types": "3.0.1",
29
29
  "cspell": "9.2.1",
30
- "knip": "5.64.0",
31
- "lefthook": "1.13.4",
30
+ "knip": "5.64.1",
31
+ "lefthook": "1.13.5",
32
32
  "only-allow": "1.2.1",
33
33
  "shellcheck": "4.1.0",
34
34
  "typescript": "5.9.2",
package/readme.md CHANGED
@@ -46,6 +46,7 @@ src/
46
46
  ├── filename-from-string.vo.ts
47
47
  ├── filename-suffix.vo.ts
48
48
  ├── filename.vo.ts
49
+ ├── height.vo.ts
49
50
  ├── hour.vo.ts
50
51
  ├── iban-mask.service.ts
51
52
  ├── iban.vo.ts
@@ -91,6 +92,7 @@ src/
91
92
  ├── week-iso-id.vo.ts
92
93
  ├── week.vo.ts
93
94
  ├── weekday.vo.ts
95
+ ├── weight.vo.ts
94
96
  ├── year-iso-id.vo.ts
95
97
  ├── year.vo.ts
96
98
  └── z-score.service.ts
@@ -0,0 +1,121 @@
1
+ import { z } from "zod/v4";
2
+ import { type RoundingStrategy, RoundToDecimal, RoundToNearest } from "./rounding.service";
3
+
4
+ const FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
5
+ const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
6
+ const NonNegativeIntegerMillimeters = FiniteNumericValue.int().min(0, {
7
+ message: "Millimeters must be an integer greater than or equal to 0",
8
+ });
9
+ const NonNegativeIntegerValue = FiniteNumericValue.int().min(0, {
10
+ message: "Value must be an integer greater than or equal to 0",
11
+ });
12
+
13
+ export enum HeightUnit {
14
+ cm = "cm",
15
+ ft_in = "ft_in",
16
+ }
17
+
18
+ export class Height {
19
+ private static readonly MILLIMETERS_PER_CENTIMETER = 10;
20
+ private static readonly MILLIMETERS_PER_INCH = 25.4;
21
+ private static readonly INCHES_PER_FOOT = 12;
22
+
23
+ private constructor(private readonly millimeters: number) {}
24
+
25
+ static fromCentimeters(centimeters: number, rounding: RoundingStrategy = new RoundToNearest()): Height {
26
+ NonNegativeNumericValue.parse(centimeters);
27
+ const mmFloat = centimeters * Height.MILLIMETERS_PER_CENTIMETER;
28
+ const mmRounded = rounding.round(mmFloat);
29
+ NonNegativeIntegerMillimeters.parse(mmRounded);
30
+ return new Height(mmRounded);
31
+ }
32
+
33
+ static fromFeetInches(feet: number, inches = 0, rounding: RoundingStrategy = new RoundToNearest()): Height {
34
+ NonNegativeNumericValue.parse(feet);
35
+ NonNegativeNumericValue.parse(inches);
36
+ const totalInches = feet * Height.INCHES_PER_FOOT + inches;
37
+ const mmFloat = totalInches * Height.MILLIMETERS_PER_INCH;
38
+ const mmRounded = rounding.round(mmFloat);
39
+ NonNegativeIntegerMillimeters.parse(mmRounded);
40
+ return new Height(mmRounded);
41
+ }
42
+
43
+ static fromMillimeters(millimeters: number, rounding: RoundingStrategy = new RoundToNearest()): Height {
44
+ NonNegativeNumericValue.parse(millimeters);
45
+ const mmRounded = rounding.round(millimeters);
46
+ NonNegativeIntegerMillimeters.parse(mmRounded);
47
+ return new Height(mmRounded);
48
+ }
49
+
50
+ static zero(): Height {
51
+ return new Height(0);
52
+ }
53
+
54
+ toMillimeters(): number {
55
+ return this.millimeters;
56
+ }
57
+
58
+ toCentimeters(rounding?: RoundingStrategy): number {
59
+ const cm = this.millimeters / Height.MILLIMETERS_PER_CENTIMETER;
60
+ return rounding ? rounding.round(cm) : cm;
61
+ }
62
+
63
+ toFeetInches(rounding: RoundingStrategy = new RoundToNearest()): {
64
+ feet: number;
65
+ inches: number;
66
+ } {
67
+ const totalInchesFloat = this.millimeters / Height.MILLIMETERS_PER_INCH;
68
+ const totalInchesRounded = rounding.round(totalInchesFloat);
69
+ const integerInches = NonNegativeIntegerValue.parse(totalInchesRounded);
70
+
71
+ const feet = (integerInches - (integerInches % Height.INCHES_PER_FOOT)) / Height.INCHES_PER_FOOT;
72
+ const inches = integerInches % Height.INCHES_PER_FOOT;
73
+
74
+ return { feet, inches };
75
+ }
76
+
77
+ format(unit: HeightUnit, roundingStrategy?: RoundingStrategy): string {
78
+ return {
79
+ [HeightUnit.cm]: () => {
80
+ const rounding = roundingStrategy ?? new RoundToDecimal(1);
81
+
82
+ return `${this.toCentimeters(rounding)} cm`;
83
+ },
84
+ [HeightUnit.ft_in]: () => {
85
+ const rounding = roundingStrategy ?? new RoundToNearest();
86
+ const { feet, inches } = this.toFeetInches(rounding);
87
+
88
+ return `${feet}′${inches}″`;
89
+ },
90
+ }[unit]();
91
+ }
92
+
93
+ equals(other: Height): boolean {
94
+ return this.millimeters === other.millimeters;
95
+ }
96
+
97
+ compare(other: Height): -1 | 0 | 1 {
98
+ if (this.equals(other)) return 0;
99
+ return this.millimeters < other.millimeters ? -1 : 1;
100
+ }
101
+
102
+ greaterThan(other: Height): boolean {
103
+ return this.millimeters > other.millimeters;
104
+ }
105
+
106
+ lessThan(other: Height): boolean {
107
+ return this.millimeters < other.millimeters;
108
+ }
109
+
110
+ isZero(): boolean {
111
+ return this.millimeters === 0;
112
+ }
113
+
114
+ toJSON(): { mm: number } {
115
+ return { mm: this.millimeters };
116
+ }
117
+
118
+ static fromJSON(input: { mm: number }): Height {
119
+ return Height.fromMillimeters(input.mm);
120
+ }
121
+ }
package/src/image.vo.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const Width = z.number().int().positive().max(10000).brand("Width");
4
- export type WidthType = z.infer<typeof Width>;
3
+ export const ImageWidth = z.number().int().positive().max(10000).brand("image-width");
4
+ export type WidthType = z.infer<typeof ImageWidth>;
5
5
 
6
- export const Height = z.number().int().positive().max(10000).brand("Height");
7
- export type HeightType = z.infer<typeof Height>;
6
+ export const ImageHeight = z.number().int().positive().max(10000).brand("image-height");
7
+ export type HeightType = z.infer<typeof ImageHeight>;
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export * from "./file-path-relative-schema.vo";
20
20
  export * from "./filename.vo";
21
21
  export * from "./filename-from-string.vo";
22
22
  export * from "./filename-suffix.vo";
23
+ export * from "./height.vo";
23
24
  export * from "./hour.vo";
24
25
  export * from "./iban.vo";
25
26
  export * from "./iban-mask.service";
@@ -65,6 +66,7 @@ export * from "./visually-unambiguous-characters-generator.service";
65
66
  export * from "./week.vo";
66
67
  export * from "./week-iso-id.vo";
67
68
  export * from "./weekday.vo";
69
+ export * from "./weight.vo";
68
70
  export * from "./year.vo";
69
71
  export * from "./year-iso-id.vo";
70
72
  export * from "./z-score.service";
@@ -0,0 +1,133 @@
1
+ import { z } from "zod/v4";
2
+ import { type RoundingStrategy, RoundToDecimal } from "./rounding.service";
3
+
4
+ const FiniteNumericValue = z.number().refine(Number.isFinite, { message: "Expected a finite number" });
5
+ const NonNegativeNumericValue = FiniteNumericValue.min(0, { message: "Must be greater than or equal to 0" });
6
+ const PositiveNumericValue = FiniteNumericValue.gt(0, { message: "Must be greater than 0" });
7
+ const NonNegativeIntegerGrams = FiniteNumericValue.int().min(0, {
8
+ message: "Grams must be an integer greater than or equal to 0",
9
+ });
10
+
11
+ export enum WeightUnit {
12
+ kg = "kg",
13
+ lb = "lb",
14
+ }
15
+
16
+ export class Weight {
17
+ private static readonly GRAMS_PER_KILOGRAM = 1_000;
18
+ private static readonly POUNDS_PER_KILOGRAM = 2.2046226218487757;
19
+ private static readonly KILOGRAMS_PER_POUND = 1 / Weight.POUNDS_PER_KILOGRAM;
20
+
21
+ private constructor(private readonly grams: number) {}
22
+
23
+ static fromKilograms(kilograms: number): Weight {
24
+ NonNegativeNumericValue.parse(kilograms);
25
+ const gramsRounded = Math.round(kilograms * Weight.GRAMS_PER_KILOGRAM);
26
+ NonNegativeIntegerGrams.parse(gramsRounded);
27
+ return new Weight(gramsRounded);
28
+ }
29
+
30
+ static fromPounds(pounds: number): Weight {
31
+ NonNegativeNumericValue.parse(pounds);
32
+ const gramsRounded = Math.round(pounds * Weight.KILOGRAMS_PER_POUND * Weight.GRAMS_PER_KILOGRAM);
33
+ NonNegativeIntegerGrams.parse(gramsRounded);
34
+ return new Weight(gramsRounded);
35
+ }
36
+
37
+ static fromGrams(grams: number): Weight {
38
+ NonNegativeNumericValue.parse(grams);
39
+ const gramsRounded = Math.round(grams);
40
+ NonNegativeIntegerGrams.parse(gramsRounded);
41
+ return new Weight(gramsRounded);
42
+ }
43
+
44
+ static zero(): Weight {
45
+ return new Weight(0);
46
+ }
47
+
48
+ toGrams(): number {
49
+ return this.grams;
50
+ }
51
+
52
+ toKilograms(rounding?: RoundingStrategy): number {
53
+ const kilograms = this.grams / Weight.GRAMS_PER_KILOGRAM;
54
+ return rounding ? rounding.round(kilograms) : kilograms;
55
+ }
56
+
57
+ toPounds(rounding?: RoundingStrategy): number {
58
+ const pounds = (this.grams / Weight.GRAMS_PER_KILOGRAM) * Weight.POUNDS_PER_KILOGRAM;
59
+ return rounding ? rounding.round(pounds) : pounds;
60
+ }
61
+
62
+ format(unit: WeightUnit, rounding: RoundingStrategy = new RoundToDecimal(2)): string {
63
+ const value = { [WeightUnit.kg]: this.toKilograms(rounding), [WeightUnit.lb]: this.toPounds(rounding) }[
64
+ unit
65
+ ];
66
+
67
+ return `${value.toString()} ${unit}`;
68
+ }
69
+
70
+ add(other: Weight): Weight {
71
+ return new Weight(this.grams + other.grams);
72
+ }
73
+
74
+ subtract(other: Weight): Weight {
75
+ const result = this.grams - other.grams;
76
+ return new Weight(result < 0 ? 0 : result);
77
+ }
78
+
79
+ multiply(factor: number): Weight {
80
+ NonNegativeNumericValue.parse(factor);
81
+ const gramsRounded = Math.round(this.grams * factor);
82
+ NonNegativeIntegerGrams.parse(gramsRounded);
83
+ return new Weight(gramsRounded);
84
+ }
85
+
86
+ divideByScalar(divisor: number): Weight {
87
+ PositiveNumericValue.parse(divisor);
88
+ const gramsRounded = Math.round(this.grams / divisor);
89
+ NonNegativeIntegerGrams.parse(gramsRounded);
90
+ return new Weight(gramsRounded);
91
+ }
92
+
93
+ equals(other: Weight): boolean {
94
+ return this.grams === other.grams;
95
+ }
96
+
97
+ compare(other: Weight): -1 | 0 | 1 {
98
+ if (this.grams === other.grams) return 0;
99
+ return this.grams < other.grams ? -1 : 1;
100
+ }
101
+
102
+ greaterThan(other: Weight): boolean {
103
+ return this.grams > other.grams;
104
+ }
105
+
106
+ greaterThanOrEqual(other: Weight): boolean {
107
+ return this.grams >= other.grams;
108
+ }
109
+
110
+ lessThan(other: Weight): boolean {
111
+ return this.grams < other.grams;
112
+ }
113
+
114
+ lessThanOrEqual(other: Weight): boolean {
115
+ return this.grams <= other.grams;
116
+ }
117
+
118
+ isZero(): boolean {
119
+ return this.grams === 0;
120
+ }
121
+
122
+ isPositive(): boolean {
123
+ return this.grams > 0;
124
+ }
125
+
126
+ toJSON(): { g: number } {
127
+ return { g: this.grams };
128
+ }
129
+
130
+ static fromJSON(input: { g: number }): Weight {
131
+ return Weight.fromGrams(input.g);
132
+ }
133
+ }