@bgord/tools 0.15.2 → 0.16.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 (119) hide show
  1. package/dist/basename.vo.d.ts +11 -2
  2. package/dist/basename.vo.js +22 -13
  3. package/dist/date-calculator.service.d.ts +2 -2
  4. package/dist/date-calculator.service.js +10 -11
  5. package/dist/date-range.vo.d.ts +1 -0
  6. package/dist/date-range.vo.js +2 -1
  7. package/dist/day-iso-id.vo.d.ts +3 -0
  8. package/dist/day-iso-id.vo.js +4 -4
  9. package/dist/day.vo.js +12 -10
  10. package/dist/directory-path-absolute.vo.d.ts +5 -0
  11. package/dist/directory-path-absolute.vo.js +11 -5
  12. package/dist/directory-path-relative.vo.d.ts +6 -0
  13. package/dist/directory-path-relative.vo.js +12 -6
  14. package/dist/dll.service.js +37 -31
  15. package/dist/extension.vo.d.ts +6 -2
  16. package/dist/extension.vo.js +11 -9
  17. package/dist/file-path-absolute-schema.vo.d.ts +5 -0
  18. package/dist/file-path-absolute-schema.vo.js +12 -6
  19. package/dist/file-path-relative-schema.vo.d.ts +6 -1
  20. package/dist/file-path-relative-schema.vo.js +10 -6
  21. package/dist/file-path.vo.js +4 -4
  22. package/dist/filename-from-string.vo.d.ts +6 -4
  23. package/dist/filename-from-string.vo.js +15 -14
  24. package/dist/filename-suffix.vo.d.ts +4 -2
  25. package/dist/filename-suffix.vo.js +5 -3
  26. package/dist/filename.vo.d.ts +2 -2
  27. package/dist/filename.vo.js +9 -9
  28. package/dist/height.vo.d.ts +6 -4
  29. package/dist/height.vo.js +56 -51
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.js +0 -1
  32. package/dist/mime.vo.d.ts +3 -1
  33. package/dist/mime.vo.js +8 -6
  34. package/dist/month-iso-id.vo.d.ts +3 -0
  35. package/dist/month-iso-id.vo.js +7 -12
  36. package/dist/month.vo.js +15 -13
  37. package/dist/object-key.vo.d.ts +5 -0
  38. package/dist/object-key.vo.js +16 -6
  39. package/dist/package-version.vo.d.ts +3 -0
  40. package/dist/package-version.vo.js +12 -34
  41. package/dist/pagination.service.d.ts +1 -1
  42. package/dist/pagination.service.js +11 -11
  43. package/dist/quarter-iso-id.vo.d.ts +3 -0
  44. package/dist/quarter-iso-id.vo.js +8 -7
  45. package/dist/rate-limiter.service.d.ts +3 -2
  46. package/dist/rate-limiter.service.js +4 -2
  47. package/dist/reordering.service.d.ts +20 -2
  48. package/dist/reordering.service.js +49 -29
  49. package/dist/revision.vo.d.ts +8 -3
  50. package/dist/revision.vo.js +13 -6
  51. package/dist/rounding.adapter.js +1 -2
  52. package/dist/size.vo.d.ts +1 -0
  53. package/dist/size.vo.js +4 -7
  54. package/dist/streak-calculator.service.d.ts +3 -4
  55. package/dist/streak-calculator.service.js +11 -17
  56. package/dist/time-zone-offset-value.vo.d.ts +1 -1
  57. package/dist/time-zone-offset-value.vo.js +1 -7
  58. package/dist/time.service.d.ts +11 -6
  59. package/dist/time.service.js +31 -18
  60. package/dist/timezone.vo.js +1 -3
  61. package/dist/tsconfig.tsbuildinfo +1 -1
  62. package/dist/week-iso-id.vo.d.ts +3 -0
  63. package/dist/week-iso-id.vo.js +4 -4
  64. package/dist/week.vo.js +1 -1
  65. package/dist/weekday.vo.d.ts +7 -6
  66. package/dist/weekday.vo.js +20 -13
  67. package/dist/weight.vo.d.ts +12 -0
  68. package/dist/weight.vo.js +37 -27
  69. package/dist/year-iso-id.vo.d.ts +3 -0
  70. package/dist/year-iso-id.vo.js +4 -6
  71. package/dist/year.vo.d.ts +2 -0
  72. package/dist/year.vo.js +4 -2
  73. package/package.json +1 -1
  74. package/readme.md +0 -1
  75. package/src/basename.vo.ts +25 -14
  76. package/src/clock.vo.ts +1 -0
  77. package/src/date-calculator.service.ts +10 -15
  78. package/src/date-range.vo.ts +3 -1
  79. package/src/day-iso-id.vo.ts +9 -10
  80. package/src/day.vo.ts +17 -10
  81. package/src/directory-path-absolute.vo.ts +12 -5
  82. package/src/directory-path-relative.vo.ts +13 -6
  83. package/src/dll.service.ts +45 -43
  84. package/src/extension.vo.ts +14 -12
  85. package/src/file-path-absolute-schema.vo.ts +15 -6
  86. package/src/file-path-relative-schema.vo.ts +13 -6
  87. package/src/file-path.vo.ts +15 -11
  88. package/src/filename-from-string.vo.ts +20 -15
  89. package/src/filename-suffix.vo.ts +8 -4
  90. package/src/filename.vo.ts +14 -15
  91. package/src/height.vo.ts +71 -53
  92. package/src/index.ts +0 -1
  93. package/src/mime.vo.ts +10 -7
  94. package/src/month-iso-id.vo.ts +10 -20
  95. package/src/month.vo.ts +19 -13
  96. package/src/object-key.vo.ts +21 -7
  97. package/src/outlier-detector.service.ts +1 -0
  98. package/src/package-version.vo.ts +18 -47
  99. package/src/pagination.service.ts +15 -13
  100. package/src/quarter-iso-id.vo.ts +11 -13
  101. package/src/quarter.vo.ts +3 -0
  102. package/src/rate-limiter.service.ts +7 -7
  103. package/src/reordering.service.ts +52 -38
  104. package/src/revision.vo.ts +17 -8
  105. package/src/rounding.adapter.ts +1 -3
  106. package/src/size.vo.ts +6 -16
  107. package/src/streak-calculator.service.ts +12 -17
  108. package/src/time-zone-offset-value.vo.ts +2 -7
  109. package/src/time.service.ts +43 -45
  110. package/src/timezone.vo.ts +1 -3
  111. package/src/week-iso-id.vo.ts +13 -14
  112. package/src/week.vo.ts +4 -2
  113. package/src/weekday.vo.ts +27 -13
  114. package/src/weight.vo.ts +49 -30
  115. package/src/year-iso-id.vo.ts +6 -9
  116. package/src/year.vo.ts +12 -2
  117. package/dist/stepper.service.d.ts +0 -23
  118. package/dist/stepper.service.js +0 -33
  119. package/src/stepper.service.ts +0 -43
@@ -4,49 +4,22 @@ type MajorType = number;
4
4
  type MinorType = number;
5
5
  type PatchType = number;
6
6
 
7
- export const PackageVersionValue = z
8
- .string()
9
- .min(1)
10
- .refine(
11
- (value) => {
12
- try {
13
- if (!value.startsWith("v")) return false;
14
-
15
- const [, version] = value.split("v");
16
- if (!version) return false;
17
-
18
- const [major, minor, patch] = version.split(".");
19
- if (!(major && minor && patch)) return false;
20
-
21
- if (
22
- !(
23
- Number.isInteger(Number(major)) &&
24
- Number.isInteger(Number(minor)) &&
25
- Number.isInteger(Number(patch))
26
- )
27
- ) {
28
- return false;
29
- }
7
+ export const PackageVersionError = { error: "package.version.error" } as const;
30
8
 
31
- return true;
32
- } catch (_error) {
33
- return false;
34
- }
35
- },
36
- { message: "package.version.error" },
37
- )
9
+ export const PackageVersionValue = z
10
+ .string(PackageVersionError)
11
+ .regex(/^v(\d+)\.(\d+)\.(\d+)$/, PackageVersionError)
38
12
  .transform((value) => {
39
- const [, version] = value.split("v");
13
+ const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(value)!;
40
14
 
41
- const [major, minor, patch] = (version as string).split(".");
15
+ const major = Number(match[1]);
16
+ const minor = Number(match[2]);
17
+ const patch = Number(match[3]);
42
18
 
43
- return {
44
- major: Number(major),
45
- minor: Number(minor),
46
- patch: Number(patch),
47
- };
19
+ return { major, minor, patch };
48
20
  })
49
21
  .brand("PackageVersionValue");
22
+
50
23
  export type PackageVersionValueType = z.infer<typeof PackageVersionValue>;
51
24
 
52
25
  export class PackageVersion {
@@ -56,7 +29,7 @@ export class PackageVersion {
56
29
  readonly patch: PatchType,
57
30
  ) {}
58
31
 
59
- isGreaterThanOrEqual(another: PackageVersion) {
32
+ isGreaterThanOrEqual(another: PackageVersion): boolean {
60
33
  if (this.major > another.major) return true;
61
34
  if (this.major < another.major) return false;
62
35
 
@@ -69,19 +42,17 @@ export class PackageVersion {
69
42
  return true;
70
43
  }
71
44
 
72
- toString() {
45
+ toString(): string {
73
46
  return `${this.major}.${this.minor}.${this.patch}`;
74
47
  }
75
48
 
76
- static fromStringWithV(value: string) {
77
- const version = PackageVersionValue.parse(value);
78
-
79
- return new PackageVersion(version.major, version.minor, version.patch);
49
+ static fromStringWithV(value: string): PackageVersion {
50
+ const parsed = PackageVersionValue.parse(value);
51
+ return new PackageVersion(parsed.major, parsed.minor, parsed.patch);
80
52
  }
81
53
 
82
- static fromString(value: string) {
83
- const version = PackageVersionValue.parse(`v${value}`);
84
-
85
- return new PackageVersion(version.major, version.minor, version.patch);
54
+ static fromString(value: string): PackageVersion {
55
+ const parsed = PackageVersionValue.parse(`v${value}`);
56
+ return new PackageVersion(parsed.major, parsed.minor, parsed.patch);
86
57
  }
87
58
  }
@@ -1,16 +1,23 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- const Take = z.number().int().gte(0);
3
+ const PaginationTakeError = { error: "pagination.take.invalid" } as const;
4
+ const PaginationSkipError = { error: "pagination.skip.invalid" } as const;
5
+ const PaginationPageError = { error: "pagination.page.invalid" } as const;
6
+
7
+ const Take = z.number(PaginationTakeError).int(PaginationTakeError).gte(1, PaginationTakeError);
8
+
4
9
  type TakeType = z.infer<typeof Take>;
5
10
 
6
- const Skip = z.number().int().gte(0);
11
+ const Skip = z.number(PaginationSkipError).int(PaginationSkipError).gte(0, PaginationSkipError);
12
+
7
13
  type SkipType = z.infer<typeof Skip>;
8
14
 
9
15
  const Page = z.coerce
10
- .number()
11
- .int()
16
+ .number(PaginationPageError)
17
+ .int(PaginationPageError)
12
18
  .transform((value) => (value <= 0 ? 1 : value))
13
19
  .default(1);
20
+
14
21
  export type PageType = z.infer<typeof Page>;
15
22
 
16
23
  export type PaginationType = { values: { take: TakeType; skip: SkipType }; page: PageType };
@@ -46,16 +53,11 @@ export class Pagination {
46
53
  }
47
54
 
48
55
  static isExhausted(config: PaginationExhaustedConfig): ExhaustedType {
49
- const lastPage = Pagination.getLastPage(config);
50
- const currentPage = config.pagination.page;
51
-
52
- return lastPage <= currentPage;
56
+ return Pagination.getLastPage(config) <= config.pagination.page;
53
57
  }
54
58
 
55
59
  private static getLastPage(config: PaginationExhaustedConfig): PageType {
56
- const lastPage = Math.ceil(config.total / config.pagination.values.take);
57
-
58
- return Page.parse(lastPage);
60
+ return Page.parse(Math.ceil(config.total / config.pagination.values.take));
59
61
  }
60
62
 
61
63
  static empty = {
@@ -70,8 +72,8 @@ export class Pagination {
70
72
  },
71
73
  };
72
74
 
73
- static getFirstPage({ take }: { take: TakeType }): PaginationType {
74
- return { values: { take, skip: Skip.parse(0) }, page: Page.parse(1) };
75
+ static getFirstPage(input: { take: TakeType }): PaginationType {
76
+ return { values: { take: Take.parse(input.take), skip: Skip.parse(0) }, page: Page.parse(1) };
75
77
  }
76
78
  }
77
79
 
@@ -1,18 +1,16 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
+ export const QuarterIsoIdError = { error: "quarter-iso-id.invalid" } as const;
4
+
3
5
  export const QuarterIsoId = z
4
- .string()
5
- .regex(/^\d{4}-Q[1-4]$/)
6
- .refine(
7
- (value) => {
8
- const [y, q] = value.split("-Q");
9
- const year = Number(y);
10
- const quarter = Number(q);
6
+ .string(QuarterIsoIdError)
7
+ .regex(/^\d{4}-Q[1-4]$/, QuarterIsoIdError)
8
+ .refine((value) => {
9
+ const [yearPart, quarterPart] = value.split("-Q");
10
+ const year = Number(yearPart);
11
+ const quarter = Number(quarterPart);
12
+
13
+ return Number.isInteger(year) && Number.isInteger(quarter) && quarter >= 1 && quarter <= 4;
14
+ }, QuarterIsoIdError);
11
15
 
12
- return (
13
- y.length === 4 && Number.isInteger(year) && Number.isInteger(quarter) && quarter >= 1 && quarter <= 4
14
- );
15
- },
16
- { message: "quarter-iso-id.invalid" },
17
- );
18
16
  export type QuarterIsoIdType = z.infer<typeof QuarterIsoId>;
package/src/quarter.vo.ts CHANGED
@@ -7,12 +7,14 @@ export class Quarter extends DateRange {
7
7
  toIsoId(): QuarterIsoIdType {
8
8
  const year = getYear(this.getStart());
9
9
  const quarter = getQuarter(this.getStart());
10
+
10
11
  return `${year}-Q${quarter}` as QuarterIsoIdType;
11
12
  }
12
13
 
13
14
  static fromTimestamp(timestamp: TimestampType): Quarter {
14
15
  const start = Timestamp.parse(startOfQuarter(timestamp).getTime());
15
16
  const end = Timestamp.parse(endOfQuarter(timestamp).getTime());
17
+
16
18
  return new Quarter(start, end);
17
19
  }
18
20
 
@@ -23,6 +25,7 @@ export class Quarter extends DateRange {
23
25
  static fromIsoId(isoId: QuarterIsoIdType): Quarter {
24
26
  const [year, quarter] = QuarterIsoId.parse(isoId).split("-Q").map(Number);
25
27
  const reference = setQuarter(new Date(Date.UTC(year, 0, 1)), quarter);
28
+
26
29
  return Quarter.fromTimestamp(Timestamp.parse(reference.getTime()));
27
30
  }
28
31
  }
@@ -1,19 +1,17 @@
1
- import type { TimeResult } from "./time.service";
2
1
  import { Timestamp, type TimestampType } from "./timestamp.vo";
3
- import type { Falsy } from "./ts-utils";
4
2
 
5
- type RateLimiterOptionsType = Pick<TimeResult, "ms">;
3
+ type RateLimiterOptionsType = { ms: TimestampType };
6
4
  type RateLimiterResultSuccessType = { allowed: true };
7
5
  type RateLimiterResultErrorType = { allowed: false; remainingMs: TimestampType };
8
6
  type RateLimiterResultType = RateLimiterResultSuccessType | RateLimiterResultErrorType;
9
7
 
10
8
  export class RateLimiter {
11
- private lastInvocationTimestampMs: Falsy<TimestampType> = null;
9
+ private lastInvocationTimestampMs: TimestampType | null = null;
12
10
 
13
11
  constructor(private readonly options: RateLimiterOptionsType) {}
14
12
 
15
13
  verify(currentTimestampMs: TimestampType): RateLimiterResultType {
16
- if (this.lastInvocationTimestampMs === null || this.lastInvocationTimestampMs === undefined) {
14
+ if (this.lastInvocationTimestampMs == null) {
17
15
  this.lastInvocationTimestampMs = currentTimestampMs;
18
16
 
19
17
  return { allowed: true };
@@ -23,10 +21,12 @@ export class RateLimiter {
23
21
 
24
22
  if (nextAllowedTimestampMs <= currentTimestampMs) {
25
23
  this.lastInvocationTimestampMs = currentTimestampMs;
26
-
27
24
  return { allowed: true };
28
25
  }
29
26
 
30
- return { allowed: false, remainingMs: Timestamp.parse(nextAllowedTimestampMs - currentTimestampMs) };
27
+ const remainingDelta = nextAllowedTimestampMs - currentTimestampMs;
28
+ const remainingMs = Timestamp.parse(remainingDelta);
29
+
30
+ return { allowed: false, remainingMs };
31
31
  }
32
32
  }
@@ -1,13 +1,25 @@
1
1
  import { z } from "zod/v4";
2
2
  import { DoublyLinkedList, Node } from "./dll.service";
3
3
 
4
- export const ReorderingItemPositionValue = z.number().int().min(0);
4
+ export const ReorderingPositionError = { error: "reordering.position.invalid" } as const;
5
+ export const ReorderingCannotFindItemError = { error: "reordering.item.not_found" } as const;
6
+ export const ReorderingCannotFindCurrentError = { error: "reordering.current_item.not_found" } as const;
7
+ export const ReorderingCannotFindTargetError = { error: "reordering.target_item.not_found" } as const;
8
+ export const ReorderingCorrelationIdError = { error: "reordering.correlation_id.invalid" } as const;
9
+ export const ReorderingItemIdError = { error: "reordering.item_id.invalid" } as const;
10
+
11
+ export const ReorderingItemPositionValue = z
12
+ .number(ReorderingPositionError)
13
+ .int(ReorderingPositionError)
14
+ .min(0, ReorderingPositionError);
5
15
  export type ReorderingItemPositionValueType = z.infer<typeof ReorderingItemPositionValue>;
6
16
 
7
- export const ReorderingCorrelationId = z.string().min(1);
17
+ export const ReorderingCorrelationId = z
18
+ .string(ReorderingCorrelationIdError)
19
+ .min(1, ReorderingCorrelationIdError);
8
20
  export type ReorderingCorrelationIdType = z.infer<typeof ReorderingCorrelationId>;
9
21
 
10
- export const ReorderingItemId = z.uuid();
22
+ export const ReorderingItemId = z.string(ReorderingItemIdError);
11
23
  export type ReorderingItemIdType = z.infer<typeof ReorderingItemId>;
12
24
 
13
25
  export const Reordering = z.object({
@@ -15,7 +27,6 @@ export const Reordering = z.object({
15
27
  id: ReorderingItemId,
16
28
  position: ReorderingItemPositionValue,
17
29
  });
18
-
19
30
  export type ReorderingType = z.infer<typeof Reordering>;
20
31
 
21
32
  export type WithReorderingPositionValue<T> = T & { position: ReorderingItemPositionValueType };
@@ -24,9 +35,8 @@ export class ReorderingPosition {
24
35
  readonly value: ReorderingItemPositionValueType;
25
36
 
26
37
  constructor(value: ReorderingItemPositionValueType) {
27
- if (!ReorderingItemPositionValue.safeParse(value).success) {
28
- throw new Error("Position is not a positive integer");
29
- }
38
+ const parsed = ReorderingItemPositionValue.safeParse(value);
39
+ if (!parsed.success) throw new Error(ReorderingPositionError.error);
30
40
 
31
41
  this.value = value;
32
42
  }
@@ -55,21 +65,19 @@ enum ReorderingTransferDirection {
55
65
 
56
66
  export class ReorderingTransfer {
57
67
  readonly id: ReorderingItem["id"];
58
-
59
68
  readonly to: ReorderingPosition;
60
69
 
61
70
  constructor(config: { id: ReorderingItem["id"]; to: ReorderingItemPositionValueType }) {
62
- this.id = config.id;
63
- this.to = new ReorderingPosition(config.to);
71
+ const id = config.id;
72
+ const to = new ReorderingPosition(config.to);
73
+
74
+ this.id = id;
75
+ this.to = to;
64
76
  }
65
77
 
66
78
  getDirection(currentPosition: ReorderingPosition): ReorderingTransferDirection {
67
79
  if (this.to.value === currentPosition.value) return ReorderingTransferDirection.noop;
68
-
69
- if (this.to.value > currentPosition.value) {
70
- return ReorderingTransferDirection.downwards;
71
- }
72
-
80
+ if (this.to.value > currentPosition.value) return ReorderingTransferDirection.downwards;
73
81
  return ReorderingTransferDirection.upwards;
74
82
  }
75
83
  }
@@ -90,62 +98,68 @@ export class ReorderingCalculator {
90
98
  }
91
99
 
92
100
  add(id: ReorderingItem["id"]): ReorderingItem {
93
- const position = new ReorderingPosition(ReorderingItemPositionValue.parse(this.dll.getSize()));
101
+ const size = this.dll.getSize();
102
+ const position = new ReorderingPosition(ReorderingItemPositionValue.parse(size));
94
103
  const item = new ReorderingItem(id, position);
95
- this.dll.append(new Node(item));
104
+ const node = new Node(item);
105
+ this.dll.append(node);
96
106
  return item;
97
107
  }
98
108
 
99
109
  delete(id: ReorderingItem["id"]) {
100
- const item = this.dll.find((x) => x.data.eq(id));
101
- if (!item) throw new Error("Cannot find Item");
110
+ const node = this.dll.find((x) => x.data.eq(id));
111
+ if (!node) throw new Error(ReorderingCannotFindItemError.error);
102
112
 
103
- this.dll.remove(item);
113
+ this.dll.remove(node);
104
114
  this.recalculate();
105
115
  }
106
116
 
107
117
  transfer(transfer: ReorderingTransfer): ReturnType<ReorderingCalculator["read"]> {
108
118
  const current = this.dll.find((node) => node.data.eq(transfer.id));
109
- const target = this.dll.find((node) => node.data.position.eq(transfer.to));
119
+ if (!current) throw new Error(ReorderingCannotFindCurrentError.error);
110
120
 
111
- if (!current) throw new Error("Cannot find current Item");
112
- if (!target) throw new Error("Cannot find target Item");
121
+ const target = this.dll.find((node) => node.data.position.eq(transfer.to));
122
+ if (!target) throw new Error(ReorderingCannotFindTargetError.error);
113
123
 
114
124
  const direction = transfer.getDirection(current.data.position);
115
-
116
125
  if (direction === ReorderingTransferDirection.noop) return this.read();
126
+
127
+ // remove first to avoid temporary invalid duplicates of positions
128
+ this.dll.remove(current);
129
+
117
130
  if (direction === ReorderingTransferDirection.upwards) {
118
- this.dll.remove(current);
119
131
  this.dll.insertBefore(current, target);
120
- this.recalculate();
121
- }
122
- if (direction === ReorderingTransferDirection.downwards) {
123
- this.dll.remove(current);
132
+ } else {
124
133
  this.dll.insertAfter(current, target);
125
- this.recalculate();
126
134
  }
135
+
136
+ this.recalculate();
127
137
  return this.read();
128
138
  }
129
139
 
130
140
  read() {
131
- const ids = Array.from(this.dll).map((item) => item.data.id);
132
- const items = Array.from(this.dll).map((item) => item.data);
141
+ const ids = Array.from(this.dll).map((node) => node.data.id);
142
+ const items = Array.from(this.dll).map((node) => node.data);
133
143
  return { ids, items };
134
144
  }
135
145
 
136
146
  private recalculate() {
137
- this.dll = ReorderingCalculator.fromArray(this.read().ids).dll;
147
+ let index = 0;
148
+ for (const node of this.dll) {
149
+ const id = node.data.id;
150
+ const position = new ReorderingPosition(index);
151
+ node.data = new ReorderingItem(id, position);
152
+ index += 1;
153
+ }
138
154
  }
139
155
  }
140
156
 
141
157
  export class ReorderingIntegrator {
142
158
  static appendPosition(reordering: ReorderingType[]) {
143
159
  return function <T extends { id: ReorderingItemIdType }>(item: T): WithReorderingPositionValue<T> {
144
- const position = ReorderingItemPositionValue.parse(
145
- reordering.find((x) => x.id === item.id)?.position ?? 0,
146
- );
147
-
148
- return { ...item, position };
160
+ const found = reordering.find((x) => x.id === item.id);
161
+ const positionValue = ReorderingItemPositionValue.parse(found?.position ?? 0);
162
+ return { ...item, position: positionValue };
149
163
  };
150
164
  }
151
165
 
@@ -1,27 +1,36 @@
1
1
  import { z } from "zod/v4";
2
2
  import type { ETag, WeakETag } from "./etags.vo";
3
3
 
4
- export const RevisionValue = z.number().int().min(0);
4
+ export const RevisionValueError = { error: "invalid.revision.value" } as const;
5
+
6
+ export const RevisionValue = z
7
+ .number(RevisionValueError)
8
+ .int(RevisionValueError)
9
+ .min(0, RevisionValueError)
10
+ .brand("RevisionValue");
11
+
5
12
  export type RevisionValueType = z.infer<typeof RevisionValue>;
6
13
 
14
+ export const RevisionInvalidErrorMessage = "revision.invalid" as const;
15
+ export const RevisionMismatchErrorMessage = "revision.mismatch" as const;
16
+
7
17
  export class Revision {
18
+ static readonly INITIAL: RevisionValueType = RevisionValue.parse(0);
19
+
8
20
  readonly value: RevisionValueType;
9
- static initial: RevisionValueType = 0;
10
21
 
11
22
  constructor(value: unknown) {
12
23
  const result = RevisionValue.safeParse(value);
13
-
14
24
  if (!result.success) throw new InvalidRevisionError();
15
-
16
25
  this.value = result.data;
17
26
  }
18
27
 
19
- matches(another: RevisionValueType): boolean {
28
+ equals(another: RevisionValueType): boolean {
20
29
  return this.value === another;
21
30
  }
22
31
 
23
32
  validate(another: RevisionValueType): void {
24
- if (!this.matches(another)) throw new RevisionMismatchError();
33
+ if (!this.equals(another)) throw new RevisionMismatchError();
25
34
  }
26
35
 
27
36
  next(): Revision {
@@ -41,14 +50,14 @@ export class Revision {
41
50
 
42
51
  export class RevisionMismatchError extends Error {
43
52
  constructor() {
44
- super();
53
+ super(RevisionMismatchErrorMessage);
45
54
  Object.setPrototypeOf(this, RevisionMismatchError.prototype);
46
55
  }
47
56
  }
48
57
 
49
58
  export class InvalidRevisionError extends Error {
50
59
  constructor() {
51
- super();
60
+ super(RevisionInvalidErrorMessage);
52
61
  Object.setPrototypeOf(this, InvalidRevisionError.prototype);
53
62
  }
54
63
  }
@@ -22,9 +22,7 @@ export const RoundingDecimalsError = "invalid.rounding.decimals" as const;
22
22
 
23
23
  export class RoundToDecimal implements RoundingPort {
24
24
  constructor(private readonly decimals: number) {
25
- if (!Number.isInteger(decimals) || decimals < 0 || decimals > 100) {
26
- throw new Error(RoundingDecimalsError);
27
- }
25
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 100) throw new Error(RoundingDecimalsError);
28
26
  }
29
27
 
30
28
  round(value: number): number {
package/src/size.vo.ts CHANGED
@@ -15,17 +15,15 @@ type SizeConfigType = { unit: SizeUnit; value: number };
15
15
 
16
16
  export class Size {
17
17
  private readonly unit: SizeUnit;
18
-
19
18
  private readonly value: SizeValueType;
20
-
21
19
  private readonly bytes: SizeValueType;
22
20
 
23
21
  private static readonly KB_MULTIPLIER = 1024;
24
-
25
22
  private static readonly MB_MULTIPLIER = 1024 * Size.KB_MULTIPLIER;
26
-
27
23
  private static readonly GB_MULTIPLIER = 1024 * Size.MB_MULTIPLIER;
28
24
 
25
+ private static readonly ROUNDER = new RoundToDecimal(2);
26
+
29
27
  constructor(config: SizeConfigType) {
30
28
  this.unit = config.unit;
31
29
  this.value = SizeValue.parse(config.value);
@@ -60,28 +58,20 @@ export class Size {
60
58
  return this.bytes;
61
59
  }
62
60
 
63
- isGreaterThan(another: Size) {
61
+ isGreaterThan(another: Size): boolean {
64
62
  return this.bytes > another.toBytes();
65
63
  }
66
64
 
67
65
  format(unit: SizeUnit): string {
68
- const rounding = new RoundToDecimal(2);
69
-
70
66
  switch (unit) {
71
67
  case SizeUnit.kB: {
72
- const kbs = rounding.round(this.bytes / Size.KB_MULTIPLIER);
73
-
74
- return `${kbs} ${SizeUnit.kB}`;
68
+ return `${Size.ROUNDER.round(this.bytes / Size.KB_MULTIPLIER)} ${SizeUnit.kB}`;
75
69
  }
76
70
  case SizeUnit.MB: {
77
- const mbs = rounding.round(this.bytes / Size.MB_MULTIPLIER);
78
-
79
- return `${mbs} ${SizeUnit.MB}`;
71
+ return `${Size.ROUNDER.round(this.bytes / Size.MB_MULTIPLIER)} ${SizeUnit.MB}`;
80
72
  }
81
73
  case SizeUnit.GB: {
82
- const gbs = rounding.round(this.bytes / Size.GB_MULTIPLIER);
83
-
84
- return `${gbs} ${SizeUnit.GB}`;
74
+ return `${Size.ROUNDER.round(this.bytes / Size.GB_MULTIPLIER)} ${SizeUnit.GB}`;
85
75
  }
86
76
  default: {
87
77
  // SizeUnit.b
@@ -1,4 +1,4 @@
1
- import { format, isAfter, isEqual, subDays } from "date-fns";
1
+ import { format, subDays } from "date-fns";
2
2
 
3
3
  type DateType = string;
4
4
  export type StreakType = { cutoff: DateType; dates: DateType[]; streak: number };
@@ -6,32 +6,27 @@ export type StreakType = { cutoff: DateType; dates: DateType[]; streak: number }
6
6
  export class StreakCalculator {
7
7
  private readonly cutoff: DateType;
8
8
 
9
- constructor() {
10
- const today = new Date();
11
- this.cutoff = StreakCalculator.format(today);
9
+ constructor(now: Date = new Date()) {
10
+ this.cutoff = StreakCalculator.format(now);
12
11
  }
13
12
 
14
- calculate(_dates: DateType[]): StreakType {
15
- const dates = Array.from(new Set(_dates));
13
+ calculate(inputDates: DateType[]): StreakType {
14
+ const dates = Array.from(new Set(inputDates));
15
+ const datesSet = new Set(dates);
16
16
 
17
17
  let streak = 0;
18
- let streakTail = this.cutoff;
18
+ let cursor = this.cutoff;
19
19
 
20
- for (let i = 0; i < dates.length; i++) {
21
- const date = dates[i] as string;
22
-
23
- if (isAfter(date, streakTail)) continue;
24
-
25
- if (isEqual(streakTail, date) || isAfter(date, streakTail)) {
26
- streakTail = StreakCalculator.format(subDays(date, 1));
27
- streak++;
28
- } else break;
20
+ while (datesSet.has(cursor)) {
21
+ streak++;
22
+ const cursorDate = new Date(`${cursor}T00:00:00Z`);
23
+ cursor = StreakCalculator.format(subDays(cursorDate, 1));
29
24
  }
30
25
 
31
26
  return { cutoff: this.cutoff, dates, streak };
32
27
  }
33
28
 
34
- static format(date: Parameters<typeof format>[0]): DateType {
29
+ static format(date: Date | number): DateType {
35
30
  return format(date, "yyyy-MM-dd");
36
31
  }
37
32
  }
@@ -1,10 +1,5 @@
1
1
  import { z } from "zod/v4";
2
2
 
3
- export const TimeZoneOffsetValue = z
4
- .string()
5
- .trim()
6
- .or(z.undefined())
7
- .transform((value) => Number(value))
8
- .transform((value) => (Number.isNaN(value) ? 0 : value))
9
- .brand("TimeZoneOffsetValue");
3
+ export const TimeZoneOffsetValue = z.coerce.number().catch(0).brand("TimeZoneOffsetValue");
4
+
10
5
  export type TimeZoneOffsetValueType = z.infer<typeof TimeZoneOffsetValue>;