@bgord/tools 0.15.1 → 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 (122) 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/language.vo.d.ts +1 -1
  33. package/dist/language.vo.js +1 -2
  34. package/dist/mime.vo.d.ts +3 -1
  35. package/dist/mime.vo.js +8 -6
  36. package/dist/month-iso-id.vo.d.ts +3 -0
  37. package/dist/month-iso-id.vo.js +7 -12
  38. package/dist/month.vo.js +15 -13
  39. package/dist/object-key.vo.d.ts +5 -0
  40. package/dist/object-key.vo.js +16 -6
  41. package/dist/package-version.vo.d.ts +3 -0
  42. package/dist/package-version.vo.js +12 -34
  43. package/dist/pagination.service.d.ts +1 -1
  44. package/dist/pagination.service.js +11 -11
  45. package/dist/quarter-iso-id.vo.d.ts +3 -0
  46. package/dist/quarter-iso-id.vo.js +8 -7
  47. package/dist/rate-limiter.service.d.ts +3 -2
  48. package/dist/rate-limiter.service.js +4 -2
  49. package/dist/reordering.service.d.ts +20 -2
  50. package/dist/reordering.service.js +49 -29
  51. package/dist/revision.vo.d.ts +8 -3
  52. package/dist/revision.vo.js +13 -6
  53. package/dist/rounding.adapter.js +1 -2
  54. package/dist/size.vo.d.ts +1 -0
  55. package/dist/size.vo.js +4 -7
  56. package/dist/streak-calculator.service.d.ts +3 -4
  57. package/dist/streak-calculator.service.js +11 -17
  58. package/dist/time-zone-offset-value.vo.d.ts +1 -1
  59. package/dist/time-zone-offset-value.vo.js +1 -7
  60. package/dist/time.service.d.ts +11 -6
  61. package/dist/time.service.js +31 -18
  62. package/dist/timezone.vo.js +1 -3
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/dist/week-iso-id.vo.d.ts +3 -0
  65. package/dist/week-iso-id.vo.js +4 -4
  66. package/dist/week.vo.js +1 -1
  67. package/dist/weekday.vo.d.ts +7 -6
  68. package/dist/weekday.vo.js +20 -13
  69. package/dist/weight.vo.d.ts +12 -0
  70. package/dist/weight.vo.js +37 -27
  71. package/dist/year-iso-id.vo.d.ts +3 -0
  72. package/dist/year-iso-id.vo.js +4 -6
  73. package/dist/year.vo.d.ts +2 -0
  74. package/dist/year.vo.js +4 -2
  75. package/package.json +1 -1
  76. package/readme.md +0 -1
  77. package/src/basename.vo.ts +25 -14
  78. package/src/clock.vo.ts +1 -0
  79. package/src/date-calculator.service.ts +10 -15
  80. package/src/date-range.vo.ts +3 -1
  81. package/src/day-iso-id.vo.ts +9 -10
  82. package/src/day.vo.ts +17 -10
  83. package/src/directory-path-absolute.vo.ts +12 -5
  84. package/src/directory-path-relative.vo.ts +13 -6
  85. package/src/dll.service.ts +45 -43
  86. package/src/extension.vo.ts +14 -12
  87. package/src/file-path-absolute-schema.vo.ts +15 -6
  88. package/src/file-path-relative-schema.vo.ts +13 -6
  89. package/src/file-path.vo.ts +15 -11
  90. package/src/filename-from-string.vo.ts +20 -15
  91. package/src/filename-suffix.vo.ts +8 -4
  92. package/src/filename.vo.ts +14 -15
  93. package/src/height.vo.ts +71 -53
  94. package/src/index.ts +0 -1
  95. package/src/language.vo.ts +1 -2
  96. package/src/mime.vo.ts +10 -7
  97. package/src/month-iso-id.vo.ts +10 -20
  98. package/src/month.vo.ts +19 -13
  99. package/src/object-key.vo.ts +21 -7
  100. package/src/outlier-detector.service.ts +1 -0
  101. package/src/package-version.vo.ts +18 -47
  102. package/src/pagination.service.ts +15 -13
  103. package/src/quarter-iso-id.vo.ts +11 -13
  104. package/src/quarter.vo.ts +3 -0
  105. package/src/rate-limiter.service.ts +7 -7
  106. package/src/reordering.service.ts +52 -38
  107. package/src/revision.vo.ts +17 -8
  108. package/src/rounding.adapter.ts +1 -3
  109. package/src/size.vo.ts +6 -16
  110. package/src/streak-calculator.service.ts +12 -17
  111. package/src/time-zone-offset-value.vo.ts +2 -7
  112. package/src/time.service.ts +43 -45
  113. package/src/timezone.vo.ts +1 -3
  114. package/src/week-iso-id.vo.ts +13 -14
  115. package/src/week.vo.ts +4 -2
  116. package/src/weekday.vo.ts +27 -13
  117. package/src/weight.vo.ts +49 -30
  118. package/src/year-iso-id.vo.ts +6 -9
  119. package/src/year.vo.ts +12 -2
  120. package/dist/stepper.service.d.ts +0 -23
  121. package/dist/stepper.service.js +0 -33
  122. package/src/stepper.service.ts +0 -43
@@ -1,3 +1,12 @@
1
1
  import { z } from "zod/v4";
2
- export declare const BasenameSchema: z.core.$ZodBranded<z.ZodString, "basename">;
3
- export type BasenameType = z.infer<typeof BasenameSchema>;
2
+ export declare const BasenameTypeError: "basename.not.string";
3
+ export declare const BasenameEmptyError: "basename.empty";
4
+ export declare const BasenameTooLongError: "basename.too.long";
5
+ export declare const BasenameSlashesForbiddenError: "basename.slashes.forbidden";
6
+ export declare const BasenameControlCharsForbiddenError: "basename.control.chars.forbidden";
7
+ export declare const BasenameDotSegmentsForbiddenError: "basename.dot.segments.forbidden";
8
+ export declare const BasenameDotfilesForbiddenError: "basename.dotfiles.forbidden";
9
+ export declare const BasenameTrailingDotForbiddenError: "basename.trailing.dot.forbidden";
10
+ export declare const BasenameBadCharsError: "basename.bad.chars";
11
+ export declare const Basename: z.core.$ZodBranded<z.ZodString, "Basename">;
12
+ export type BasenameType = z.infer<typeof Basename>;
@@ -1,16 +1,25 @@
1
1
  import { z } from "zod/v4";
2
- export const BasenameSchema = z
3
- .string()
2
+ export const BasenameTypeError = "basename.not.string";
3
+ export const BasenameEmptyError = "basename.empty";
4
+ export const BasenameTooLongError = "basename.too.long";
5
+ export const BasenameSlashesForbiddenError = "basename.slashes.forbidden";
6
+ export const BasenameControlCharsForbiddenError = "basename.control.chars.forbidden";
7
+ export const BasenameDotSegmentsForbiddenError = "basename.dot.segments.forbidden";
8
+ export const BasenameDotfilesForbiddenError = "basename.dotfiles.forbidden";
9
+ export const BasenameTrailingDotForbiddenError = "basename.trailing.dot.forbidden";
10
+ export const BasenameBadCharsError = "basename.bad.chars";
11
+ export const Basename = z
12
+ .string(BasenameTypeError)
4
13
  .trim()
5
- .min(1, "basename_empty")
6
- .max(128, "basename_too_long")
7
- .refine((s) => !/[/\\]/.test(s), "basename_slashes_forbidden")
14
+ .min(1, BasenameEmptyError)
15
+ .max(128, BasenameTooLongError)
16
+ .refine((s) => !/[/\\]/.test(s), BasenameSlashesForbiddenError)
17
+ // dot-related checks: dot-segments first for specific errors…
8
18
  // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
9
- .refine((s) => !/[\u0000-\u001F\u007F]/.test(s), "basename_control_chars_forbidden")
10
- // check dot-segments FIRST so "." / ".." get the intended error
11
- .refine((s) => s !== "." && s !== "..", "basename_dot_segments_forbidden")
12
- // then disallow any other dotfile (".env", ".gitignore", etc.)
13
- .refine((s) => !s.startsWith("."), "basename_dotfiles_forbidden")
14
- .refine((s) => !s.endsWith("."), "basename_trailing_dot_forbidden")
15
- .regex(/^[A-Za-z0-9._-]+$/, "basename_bad_chars")
16
- .brand("basename");
19
+ .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), BasenameControlCharsForbiddenError)
20
+ .refine((value) => value !== "." && value !== "..", BasenameDotSegmentsForbiddenError)
21
+ // …then any other dotfile
22
+ .refine((value) => !value.startsWith("."), BasenameDotfilesForbiddenError)
23
+ .refine((value) => !value.endsWith("."), BasenameTrailingDotForbiddenError)
24
+ .regex(/^[A-Za-z0-9._-]+$/, BasenameBadCharsError)
25
+ .brand("Basename");
@@ -1,9 +1,9 @@
1
- import type { TimestampType } from "./timestamp.vo";
1
+ import { type TimestampType } from "./timestamp.vo";
2
2
  type GetStartOfDayTsInTzConfigType = {
3
3
  now: TimestampType;
4
4
  timeZoneOffsetMs: number;
5
5
  };
6
6
  export declare class DateCalculator {
7
- static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType): number;
7
+ static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType): TimestampType;
8
8
  }
9
9
  export {};
@@ -1,16 +1,15 @@
1
1
  import { Time } from "./time.service";
2
+ import { Timestamp } from "./timestamp.vo";
2
3
  export class DateCalculator {
3
4
  static getStartOfDayTsInTz(config) {
4
- const startOfDayUTC = new Date();
5
- startOfDayUTC.setUTCHours(0, 0, 0, 0);
6
- const startOfDayInTimeZone = startOfDayUTC.getTime() + config.timeZoneOffsetMs;
7
- const timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay = (config.now - startOfDayInTimeZone) % Time.Days(1).ms;
8
- if (timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay >= Time.Days(1).ms) {
9
- return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay + Time.Days(1).ms;
10
- }
11
- if (timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay >= 0) {
12
- return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay;
13
- }
14
- return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay - Time.Days(1).ms;
5
+ const dayMs = Time.Days(1).ms;
6
+ // UTC midnight for the UTC date of `now`
7
+ const utcMidnight = Math.floor(config.now / dayMs) * dayMs;
8
+ // Candidate start of the local day (in UTC), anchored to the same UTC date
9
+ let start = utcMidnight + config.timeZoneOffsetMs;
10
+ // If the candidate is in the future relative to `now`, it means local midnight was "yesterday" in UTC.
11
+ if (start > config.now)
12
+ start -= dayMs;
13
+ return Timestamp.parse(start);
15
14
  }
16
15
  }
@@ -1,4 +1,5 @@
1
1
  import type { TimestampType } from "./timestamp.vo";
2
+ export declare const DateRangeInvalidError: "invalid.date.range";
2
3
  export declare class DateRange {
3
4
  private readonly start;
4
5
  private readonly end;
@@ -1,3 +1,4 @@
1
+ export const DateRangeInvalidError = "invalid.date.range";
1
2
  export class DateRange {
2
3
  start;
3
4
  end;
@@ -5,7 +6,7 @@ export class DateRange {
5
6
  this.start = start;
6
7
  this.end = end;
7
8
  if (start > end)
8
- throw new Error("Invalid date range");
9
+ throw new Error(DateRangeInvalidError);
9
10
  }
10
11
  getStart() {
11
12
  return this.start;
@@ -1,3 +1,6 @@
1
1
  import { z } from "zod/v4";
2
+ export declare const DayIsoIdError: {
3
+ readonly error: "invalid.day.iso.id";
4
+ };
2
5
  export declare const DayIsoId: z.ZodString;
3
6
  export type DayIsoIdType = z.infer<typeof DayIsoId>;
@@ -1,10 +1,10 @@
1
1
  import { isValid, parseISO } from "date-fns";
2
2
  import { z } from "zod/v4";
3
+ export const DayIsoIdError = { error: "invalid.day.iso.id" };
3
4
  export const DayIsoId = z
4
- .string()
5
- // 4-digit year, 2-digit month, 2-digit day
6
- .regex(/^\d{4}-\d{2}-\d{2}$/)
5
+ .string(DayIsoIdError)
6
+ .regex(/^\d{4}-\d{2}-\d{2}$/, DayIsoIdError)
7
7
  .refine((value) => {
8
8
  const date = parseISO(value);
9
9
  return isValid(date) && value === date.toISOString().slice(0, 10);
10
- }, { message: "day-iso-id.invalid" });
10
+ }, DayIsoIdError);
package/dist/day.vo.js CHANGED
@@ -1,4 +1,3 @@
1
- import { addDays, endOfDay, startOfDay } from "date-fns";
2
1
  import { DateRange } from "./date-range.vo";
3
2
  import { DayIsoId } from "./day-iso-id.vo";
4
3
  import { Time } from "./time.service";
@@ -8,31 +7,34 @@ export class Day extends DateRange {
8
7
  super(start, end);
9
8
  }
10
9
  toIsoId() {
11
- return new Date(this.getStart() + Time.Hours(12).ms).toISOString().slice(0, 10);
10
+ const midday = this.getStart() + Time.Hours(12).ms;
11
+ return new Date(midday).toISOString().slice(0, 10);
12
12
  }
13
13
  previous() {
14
- const shifted = addDays(new Date(this.getStart()), -1).getTime();
14
+ const shifted = this.getStart() - Time.Days(1).ms;
15
15
  return Day.fromTimestamp(Timestamp.parse(shifted));
16
16
  }
17
17
  next() {
18
- const shifted = addDays(new Date(this.getStart()), 1).getTime();
18
+ const shifted = this.getStart() + Time.Days(1).ms;
19
19
  return Day.fromTimestamp(Timestamp.parse(shifted));
20
20
  }
21
21
  shift(count) {
22
- const shifted = addDays(new Date(this.getStart()), count).getTime();
22
+ const shifted = this.getStart() + count * Time.Days(1).ms;
23
23
  return Day.fromTimestamp(Timestamp.parse(shifted));
24
24
  }
25
25
  static fromTimestamp(timestamp) {
26
- const start = Timestamp.parse(startOfDay(timestamp).getTime());
27
- const end = Timestamp.parse(endOfDay(timestamp).getTime());
28
- return new Day(start, end);
26
+ const date = new Date(timestamp);
27
+ const startUtc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
28
+ const endUtc = startUtc + Time.Days(1).ms - 1;
29
+ return new Day(Timestamp.parse(startUtc), Timestamp.parse(endUtc));
29
30
  }
30
31
  static fromNow(now) {
31
32
  return Day.fromTimestamp(now);
32
33
  }
33
34
  static fromIsoId(isoId) {
34
35
  const [year, month, day] = DayIsoId.parse(isoId).split("-").map(Number);
35
- const reference = new Date(Date.UTC(year, month - 1, day));
36
- return Day.fromTimestamp(Timestamp.parse(reference.getTime()));
36
+ const startUtc = Date.UTC(year, month - 1, day);
37
+ const endUtc = startUtc + Time.Days(1).ms - 1;
38
+ return new Day(Timestamp.parse(startUtc), Timestamp.parse(endUtc));
37
39
  }
38
40
  }
@@ -1,3 +1,8 @@
1
1
  import { z } from "zod/v4";
2
+ export declare const AbsDirTypeError: "abs_dir.not.string";
3
+ export declare const AbsDirMustStartWithSlashError: "abs_dir_must_start_with_slash";
4
+ export declare const AbsDirBackslashForbiddenError: "abs_dir_backslash_forbidden";
5
+ export declare const AbsDirControlCharsForbiddenError: "abs_dir_control_chars_forbidden";
6
+ export declare const AbsDirBadSegmentsError: "abs_dir_bad_segments";
2
7
  export declare const DirectoryPathAbsoluteSchema: z.core.$ZodBranded<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>, "directory_path_absolute">;
3
8
  export type DirectoryPathAbsoluteType = z.infer<typeof DirectoryPathAbsoluteSchema>;
@@ -1,11 +1,17 @@
1
1
  import { z } from "zod/v4";
2
+ export const AbsDirTypeError = "abs_dir.not.string";
3
+ export const AbsDirMustStartWithSlashError = "abs_dir_must_start_with_slash";
4
+ export const AbsDirBackslashForbiddenError = "abs_dir_backslash_forbidden";
5
+ export const AbsDirControlCharsForbiddenError = "abs_dir_control_chars_forbidden";
6
+ export const AbsDirBadSegmentsError = "abs_dir_bad_segments";
2
7
  export const DirectoryPathAbsoluteSchema = z
3
- .string()
8
+ .string(AbsDirTypeError)
4
9
  .trim()
5
- .refine((value) => value.startsWith("/"), "abs_dir_must_start_with_slash")
6
- .refine((value) => !value.includes("\\"), "abs_dir_backslash_forbidden")
10
+ .refine((value) => value.startsWith("/"), AbsDirMustStartWithSlashError)
11
+ .refine((value) => !value.includes("\\"), AbsDirBackslashForbiddenError)
7
12
  // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
8
- .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), "abs_dir_control_chars_forbidden")
13
+ .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), AbsDirControlCharsForbiddenError)
14
+ // collapse duplicate slashes, then drop trailing slash unless it's the root "/"
9
15
  .transform((value) => value.replace(/\/{2,}/g, "/"))
10
16
  .transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value))
11
17
  .refine((value) => {
@@ -13,5 +19,5 @@ export const DirectoryPathAbsoluteSchema = z
13
19
  return true;
14
20
  const segments = value.slice(1).split("/");
15
21
  return segments.every((segment) => segment.length > 0 && /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== "..");
16
- }, "abs_dir_bad_segments")
22
+ }, AbsDirBadSegmentsError)
17
23
  .brand("directory_path_absolute");
@@ -1,3 +1,9 @@
1
1
  import { z } from "zod/v4";
2
+ export declare const RelDirTypeError: "rel_dir.not.string";
3
+ export declare const RelDirMustNotStartWithSlashError: "rel_dir_must_not_start_with_slash";
4
+ export declare const RelDirBackslashForbiddenError: "rel_dir_backslash_forbidden";
5
+ export declare const RelDirControlCharsForbiddenError: "rel_dir_control_chars_forbidden";
6
+ export declare const RelDirEmptyError: "rel_dir_empty";
7
+ export declare const RelDirBadSegmentsError: "rel_dir_bad_segments";
2
8
  export declare const DirectoryPathRelativeSchema: z.core.$ZodBranded<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>, "directory_path_relative">;
3
9
  export type DirectoryPathRelativeType = z.infer<typeof DirectoryPathRelativeSchema>;
@@ -1,15 +1,21 @@
1
1
  import { z } from "zod/v4";
2
+ export const RelDirTypeError = "rel_dir.not.string";
3
+ export const RelDirMustNotStartWithSlashError = "rel_dir_must_not_start_with_slash";
4
+ export const RelDirBackslashForbiddenError = "rel_dir_backslash_forbidden";
5
+ export const RelDirControlCharsForbiddenError = "rel_dir_control_chars_forbidden";
6
+ export const RelDirEmptyError = "rel_dir_empty";
7
+ export const RelDirBadSegmentsError = "rel_dir_bad_segments";
2
8
  export const DirectoryPathRelativeSchema = z
3
- .string()
9
+ .string(RelDirTypeError)
4
10
  .trim()
5
- .refine((value) => !value.startsWith("/"), "rel_dir_must_not_start_with_slash")
6
- .refine((value) => !value.includes("\\"), "rel_dir_backslash_forbidden")
11
+ .refine((value) => !value.startsWith("/"), RelDirMustNotStartWithSlashError)
12
+ .refine((value) => !value.includes("\\"), RelDirBackslashForbiddenError)
7
13
  // biome-ignore lint: lint/suspicious/noControlCharactersInRegex
8
- .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), "rel_dir_control_chars_forbidden")
14
+ .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), RelDirControlCharsForbiddenError)
9
15
  .transform((value) => value.replace(/\/{2,}/g, "/"))
10
16
  .transform((value) => value.replace(/^\/+|\/+$/g, ""))
11
- .refine((value) => value.length > 0, "rel_dir_empty")
17
+ .refine((value) => value.length > 0, RelDirEmptyError)
12
18
  .refine((value) => value
13
19
  .split("/")
14
- .every((segment) => /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== ".."), "rel_dir_bad_segments")
20
+ .every((segment) => /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== ".."), RelDirBadSegmentsError)
15
21
  .brand("directory_path_relative");
@@ -7,21 +7,19 @@ export class Node {
7
7
  }
8
8
  forward(n) {
9
9
  let currentNode = this;
10
- for (let i = 0; i < n; i++) {
11
- if (currentNode === null) {
12
- return currentNode;
13
- }
10
+ let steps = n;
11
+ while (steps > 0 && currentNode) {
14
12
  currentNode = currentNode.next;
13
+ steps -= 1;
15
14
  }
16
15
  return currentNode;
17
16
  }
18
17
  backward(n) {
19
18
  let currentNode = this;
20
- for (let i = 0; i < n; i++) {
21
- if (currentNode === null) {
22
- return currentNode;
23
- }
19
+ let steps = n;
20
+ while (steps > 0 && currentNode) {
24
21
  currentNode = currentNode.prev;
22
+ steps -= 1;
25
23
  }
26
24
  return currentNode;
27
25
  }
@@ -44,30 +42,28 @@ export class DoublyLinkedList {
44
42
  return this.tail;
45
43
  }
46
44
  append(node) {
47
- if (this.head === null || this.tail === null) {
48
- this.size++;
45
+ if (this.tail === null) {
49
46
  this.head = node;
50
47
  this.tail = node;
51
48
  }
52
49
  else {
53
- this.size++;
54
50
  this.tail.next = node;
55
51
  node.prev = this.tail;
56
52
  this.tail = node;
57
53
  }
54
+ this.size += 1;
58
55
  }
59
56
  prepend(node) {
60
- if (this.head === null || this.tail === null) {
61
- this.size++;
57
+ if (this.head === null) {
62
58
  this.head = node;
63
59
  this.tail = node;
64
60
  }
65
61
  else {
66
- this.size++;
67
62
  node.next = this.head;
68
63
  this.head.prev = node;
69
64
  this.head = node;
70
65
  }
66
+ this.size += 1;
71
67
  }
72
68
  clear() {
73
69
  this.size = 0;
@@ -87,43 +83,53 @@ export class DoublyLinkedList {
87
83
  else {
88
84
  this.tail = node.prev;
89
85
  }
90
- this.size--;
86
+ this.size -= 1;
91
87
  node.prev = null;
92
88
  node.next = null;
93
89
  }
94
90
  insertAfter(node, target) {
95
91
  if (target === this.tail) {
96
92
  this.append(node);
93
+ return;
97
94
  }
98
- else {
99
- this.size++;
100
- node.prev = target;
101
- node.next = target.next;
102
- // biome-ignore lint: lint/style/noNonNullAssertion
103
- target.next.prev = node;
104
- target.next = node;
95
+ const nextNode = target.next;
96
+ this.size += 1;
97
+ node.prev = target;
98
+ node.next = nextNode;
99
+ if (nextNode) {
100
+ nextNode.prev = node;
105
101
  }
102
+ target.next = node;
106
103
  }
107
104
  insertBefore(node, target) {
108
105
  if (target === this.head) {
109
106
  this.prepend(node);
107
+ return;
110
108
  }
111
- else {
112
- this.size++;
113
- node.next = target;
114
- node.prev = target.prev;
115
- // biome-ignore lint: lint/style/noNonNullAssertion
116
- target.prev.next = node;
117
- target.prev = node;
109
+ const prevNode = target.prev;
110
+ this.size += 1;
111
+ node.next = target;
112
+ node.prev = prevNode;
113
+ if (prevNode) {
114
+ prevNode.next = node;
118
115
  }
116
+ target.prev = node;
119
117
  }
120
118
  find(callback) {
121
- return Array.from(this).find(callback) ?? null;
119
+ let current = this.head;
120
+ while (current) {
121
+ if (callback(current))
122
+ return current;
123
+ current = current.next;
124
+ }
125
+ return null;
122
126
  }
123
127
  reverse() {
124
128
  [this.head, this.tail] = [this.tail, this.head];
125
129
  for (const node of this) {
126
- [node.prev, node.next] = [node.next, node.prev];
130
+ const originalNext = node.next;
131
+ node.next = node.prev;
132
+ node.prev = originalNext;
127
133
  }
128
134
  }
129
135
  toArray() {
@@ -1,3 +1,7 @@
1
1
  import { z } from "zod/v4";
2
- export declare const ExtensionSchema: z.core.$ZodBranded<z.ZodPipe<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>, z.ZodString>, "extension">;
3
- export type ExtensionType = z.infer<typeof ExtensionSchema>;
2
+ export declare const ExtensionTypeError: "extension.not.string";
3
+ export declare const ExtensionEmptyError: "extension.empty";
4
+ export declare const ExtensionTooLongError: "extension.too.long";
5
+ export declare const ExtensionBadCharsError: "extension.bad.chars";
6
+ export declare const Extension: z.core.$ZodBranded<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, "Extension">;
7
+ export type ExtensionType = z.infer<typeof Extension>;
@@ -1,12 +1,14 @@
1
1
  import { z } from "zod/v4";
2
- export const ExtensionSchema = z
3
- .string()
2
+ export const ExtensionTypeError = "extension.not.string";
3
+ export const ExtensionEmptyError = "extension.empty";
4
+ export const ExtensionTooLongError = "extension.too.long";
5
+ export const ExtensionBadCharsError = "extension.bad.chars";
6
+ export const Extension = z
7
+ .string(ExtensionTypeError)
4
8
  .trim()
9
+ .toLowerCase()
5
10
  .transform((value) => (value.startsWith(".") ? value.slice(1) : value))
6
- .transform((value) => value.toLowerCase())
7
- .pipe(z
8
- .string()
9
- .min(1, "extension_empty")
10
- .max(16, "extension_too_long")
11
- .regex(/^[a-z0-9]+$/, "extension_bad_chars"))
12
- .brand("extension");
11
+ .refine((value) => value.length >= 1, ExtensionEmptyError)
12
+ .refine((value) => value.length <= 16, ExtensionTooLongError)
13
+ .refine((value) => /^[a-z0-9]+$/.test(value), ExtensionBadCharsError)
14
+ .brand("Extension");
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod/v4";
2
2
  import { Filename } from "./filename.vo";
3
+ export declare const AbsFilePathTypeError: "abs.file.path.not.string";
4
+ export declare const AbsFilePathMustStartWithSlashError: "abs_file_path_must_start_with_slash";
5
+ export declare const AbsFilePathBackslashForbiddenError: "abs_file_path_backslash_forbidden";
6
+ export declare const AbsFilePathMissingFilenameError: "abs_file_path_missing_filename";
3
7
  export declare const FilePathAbsoluteSchema: z.ZodPipe<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>, z.ZodTransform<{
4
8
  directory: string & z.core.$brand<"directory_path_absolute">;
5
9
  filename: Filename;
6
10
  }, string>>;
11
+ export type FilePathAbsoluteType = z.infer<typeof FilePathAbsoluteSchema>;
@@ -1,14 +1,20 @@
1
1
  import { z } from "zod/v4";
2
2
  import { DirectoryPathAbsoluteSchema } from "./directory-path-absolute.vo";
3
3
  import { Filename } from "./filename.vo";
4
+ export const AbsFilePathTypeError = "abs.file.path.not.string";
5
+ export const AbsFilePathMustStartWithSlashError = "abs_file_path_must_start_with_slash";
6
+ export const AbsFilePathBackslashForbiddenError = "abs_file_path_backslash_forbidden";
7
+ export const AbsFilePathMissingFilenameError = "abs_file_path_missing_filename";
4
8
  export const FilePathAbsoluteSchema = z
5
- .string()
9
+ .string(AbsFilePathTypeError)
6
10
  .trim()
7
- .refine((value) => value.startsWith("/"), "abs_file_path_must_start_with_slash")
8
- .refine((value) => !value.includes("\\"), "abs_file_path_backslash_forbidden")
9
- .transform((value) => value.replace(/\/{2,}/g, "/")) // collapse //
10
- .transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value)) // keep "/" as-is
11
- .refine((value) => value !== "/", "abs_file_path_missing_filename")
11
+ .refine((value) => value.startsWith("/"), AbsFilePathMustStartWithSlashError)
12
+ .refine((value) => !value.includes("\\"), AbsFilePathBackslashForbiddenError)
13
+ // collapse duplicate slashes
14
+ .transform((value) => value.replace(/\/{2,}/g, "/"))
15
+ // keep "/" as-is; otherwise remove a trailing slash
16
+ .transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value))
17
+ .refine((value) => value !== "/", AbsFilePathMissingFilenameError)
12
18
  .transform((normalized) => {
13
19
  const lastSlashIndex = normalized.lastIndexOf("/");
14
20
  const directoryCandidate = lastSlashIndex === 0 ? "/" : normalized.slice(0, lastSlashIndex);
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod/v4";
2
2
  import { Filename } from "./filename.vo";
3
- export declare const FilePathRelativeSchema: z.ZodPipe<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>, z.ZodTransform<{
3
+ export declare const RelFilePathTypeError: "rel.file.path.not.string";
4
+ export declare const RelFilePathMustNotStartWithSlashError: "rel_file_path_must_not_start_with_slash";
5
+ export declare const RelFilePathBackslashForbiddenError: "rel_file_path_backslash_forbidden";
6
+ export declare const RelFilePathRequiresDirectoryError: "rel_file_path_requires_directory";
7
+ export declare const FilePathRelativeSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodTransform<{
4
8
  directory: string & z.core.$brand<"directory_path_relative">;
5
9
  filename: Filename;
6
10
  }, string>>;
11
+ export type FilePathRelativeType = z.infer<typeof FilePathRelativeSchema>;
@@ -1,14 +1,18 @@
1
1
  import { z } from "zod/v4";
2
2
  import { DirectoryPathRelativeSchema } from "./directory-path-relative.vo";
3
3
  import { Filename } from "./filename.vo";
4
+ export const RelFilePathTypeError = "rel.file.path.not.string";
5
+ export const RelFilePathMustNotStartWithSlashError = "rel_file_path_must_not_start_with_slash";
6
+ export const RelFilePathBackslashForbiddenError = "rel_file_path_backslash_forbidden";
7
+ export const RelFilePathRequiresDirectoryError = "rel_file_path_requires_directory";
4
8
  export const FilePathRelativeSchema = z
5
- .string()
9
+ .string(RelFilePathTypeError)
6
10
  .trim()
7
- .refine((value) => !value.startsWith("/"), "rel_file_path_must_not_start_with_slash")
8
- .refine((value) => !value.includes("\\"), "rel_file_path_backslash_forbidden")
9
- .transform((value) => value.replace(/\/{2,}/g, "/")) // collapse //
10
- .transform((value) => value.replace(/^\/+|\/+$/g, "")) // trim leading/trailing slashes
11
- .refine((value) => value.includes("/"), "rel_file_path_requires_directory")
11
+ .refine((value) => !value.startsWith("/"), RelFilePathMustNotStartWithSlashError)
12
+ .refine((value) => !value.includes("\\"), RelFilePathBackslashForbiddenError)
13
+ // collapse duplicate slashes, then trim leading/trailing slashes
14
+ .transform((value) => value.replace(/\/{2,}/g, "/").replace(/^\/+|\/+$/g, ""))
15
+ .refine((value) => value.includes("/"), RelFilePathRequiresDirectoryError)
12
16
  .transform((normalized) => {
13
17
  const lastSlashIndex = normalized.lastIndexOf("/");
14
18
  const directoryCandidate = normalized.slice(0, lastSlashIndex);
@@ -17,8 +17,8 @@ export class FilePathRelative {
17
17
  return new FilePathRelative(directory, filename);
18
18
  }
19
19
  static fromString(pathCandidate) {
20
- const { directory, filename } = FilePathRelativeSchema.parse(pathCandidate);
21
- return new FilePathRelative(directory, filename);
20
+ const parsed = FilePathRelativeSchema.parse(pathCandidate);
21
+ return new FilePathRelative(parsed.directory, parsed.filename);
22
22
  }
23
23
  get() {
24
24
  return `${this.directory}/${this.filename.get()}`;
@@ -54,8 +54,8 @@ export class FilePathAbsolute {
54
54
  return new FilePathAbsolute(directory, filename);
55
55
  }
56
56
  static fromString(pathCandidate) {
57
- const { directory, filename } = FilePathAbsoluteSchema.parse(pathCandidate);
58
- return new FilePathAbsolute(directory, filename);
57
+ const parsed = FilePathAbsoluteSchema.parse(pathCandidate);
58
+ return new FilePathAbsolute(parsed.directory, parsed.filename);
59
59
  }
60
60
  get() {
61
61
  if (this.directory === "/")
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod/v4";
2
- export declare const FilenameFromStringSchema: z.ZodPipe<z.ZodString, z.ZodTransform<{
3
- basename: string & z.core.$brand<"basename">;
4
- extension: string & z.core.$brand<"extension">;
2
+ export declare const FilenameTypeError: "filename.not.string";
3
+ export declare const FilenameInvalidError: "filename.invalid";
4
+ export declare const FilenameFromString: z.ZodPipe<z.ZodString, z.ZodTransform<{
5
+ basename: string & z.core.$brand<"Basename">;
6
+ extension: string & z.core.$brand<"Extension">;
5
7
  }, string>>;
6
- export type FilenameFromString = z.infer<typeof FilenameFromStringSchema>;
8
+ export type FilenameFromStringType = z.infer<typeof FilenameFromString>;
@@ -1,17 +1,18 @@
1
1
  import { z } from "zod/v4";
2
- import { BasenameSchema } from "./basename.vo";
3
- import { ExtensionSchema } from "./extension.vo";
4
- export const FilenameFromStringSchema = z
5
- .string()
2
+ import { Basename } from "./basename.vo";
3
+ import { Extension } from "./extension.vo";
4
+ export const FilenameTypeError = "filename.not.string";
5
+ export const FilenameInvalidError = "filename.invalid";
6
+ export const FilenameFromString = z
7
+ .string(FilenameTypeError)
6
8
  .trim()
7
- .refine((string) => {
8
- const index = string.lastIndexOf(".");
9
- return index > 0 && index < string.length - 1;
10
- }, "filename_invalid")
11
- // split and validate parts using existing schemas
12
- .transform((string) => {
13
- const index = string.lastIndexOf(".");
14
- const base = BasenameSchema.parse(string.slice(0, index));
15
- const extension = ExtensionSchema.parse(string.slice(index + 1));
16
- return { basename: base, extension: extension };
9
+ .refine((value) => {
10
+ const index = value.lastIndexOf(".");
11
+ return index > 0 && index < value.length - 1;
12
+ }, FilenameInvalidError)
13
+ .transform((value) => {
14
+ const index = value.lastIndexOf(".");
15
+ const basename = Basename.parse(value.slice(0, index));
16
+ const extension = Extension.parse(value.slice(index + 1));
17
+ return { basename, extension };
17
18
  });
@@ -1,3 +1,5 @@
1
1
  import { z } from "zod/v4";
2
- export declare const FilenameSuffixSchema: z.core.$ZodBranded<z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodString>, "basename_suffix">;
3
- export type FilenameSuffixSchemaType = z.infer<typeof FilenameSuffixSchema>;
2
+ export declare const FilenameSuffixTypeError: "suffix.not.string";
3
+ export declare const FilenameSuffixTooLongError: "suffix_too_long";
4
+ export declare const FilenameSuffix: z.core.$ZodBranded<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, "basename_suffix">;
5
+ export type FilenameSuffixType = z.infer<typeof FilenameSuffix>;
@@ -1,7 +1,9 @@
1
1
  import { z } from "zod/v4";
2
- export const FilenameSuffixSchema = z
3
- .string()
2
+ export const FilenameSuffixTypeError = "suffix.not.string";
3
+ export const FilenameSuffixTooLongError = "suffix_too_long";
4
+ export const FilenameSuffix = z
5
+ .string(FilenameSuffixTypeError)
4
6
  .trim()
5
7
  .transform((value) => value.replace(/[^A-Za-z0-9_-]/g, ""))
6
- .pipe(z.string().max(32, "suffix_too_long"))
8
+ .refine((value) => value.length <= 32, FilenameSuffixTooLongError)
7
9
  .brand("basename_suffix");