@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.
- package/dist/basename.vo.d.ts +11 -2
- package/dist/basename.vo.js +22 -13
- package/dist/date-calculator.service.d.ts +2 -2
- package/dist/date-calculator.service.js +10 -11
- package/dist/date-range.vo.d.ts +1 -0
- package/dist/date-range.vo.js +2 -1
- package/dist/day-iso-id.vo.d.ts +3 -0
- package/dist/day-iso-id.vo.js +4 -4
- package/dist/day.vo.js +12 -10
- package/dist/directory-path-absolute.vo.d.ts +5 -0
- package/dist/directory-path-absolute.vo.js +11 -5
- package/dist/directory-path-relative.vo.d.ts +6 -0
- package/dist/directory-path-relative.vo.js +12 -6
- package/dist/dll.service.js +37 -31
- package/dist/extension.vo.d.ts +6 -2
- package/dist/extension.vo.js +11 -9
- package/dist/file-path-absolute-schema.vo.d.ts +5 -0
- package/dist/file-path-absolute-schema.vo.js +12 -6
- package/dist/file-path-relative-schema.vo.d.ts +6 -1
- package/dist/file-path-relative-schema.vo.js +10 -6
- package/dist/file-path.vo.js +4 -4
- package/dist/filename-from-string.vo.d.ts +6 -4
- package/dist/filename-from-string.vo.js +15 -14
- package/dist/filename-suffix.vo.d.ts +4 -2
- package/dist/filename-suffix.vo.js +5 -3
- package/dist/filename.vo.d.ts +2 -2
- package/dist/filename.vo.js +9 -9
- package/dist/height.vo.d.ts +6 -4
- package/dist/height.vo.js +56 -51
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mime.vo.d.ts +3 -1
- package/dist/mime.vo.js +8 -6
- package/dist/month-iso-id.vo.d.ts +3 -0
- package/dist/month-iso-id.vo.js +7 -12
- package/dist/month.vo.js +15 -13
- package/dist/object-key.vo.d.ts +5 -0
- package/dist/object-key.vo.js +16 -6
- package/dist/package-version.vo.d.ts +3 -0
- package/dist/package-version.vo.js +12 -34
- package/dist/pagination.service.d.ts +1 -1
- package/dist/pagination.service.js +11 -11
- package/dist/quarter-iso-id.vo.d.ts +3 -0
- package/dist/quarter-iso-id.vo.js +8 -7
- package/dist/rate-limiter.service.d.ts +3 -2
- package/dist/rate-limiter.service.js +4 -2
- package/dist/reordering.service.d.ts +20 -2
- package/dist/reordering.service.js +49 -29
- package/dist/revision.vo.d.ts +8 -3
- package/dist/revision.vo.js +13 -6
- package/dist/rounding.adapter.js +1 -2
- package/dist/size.vo.d.ts +1 -0
- package/dist/size.vo.js +4 -7
- package/dist/streak-calculator.service.d.ts +3 -4
- package/dist/streak-calculator.service.js +11 -17
- package/dist/time-zone-offset-value.vo.d.ts +1 -1
- package/dist/time-zone-offset-value.vo.js +1 -7
- package/dist/time.service.d.ts +11 -6
- package/dist/time.service.js +31 -18
- package/dist/timezone.vo.js +1 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/week-iso-id.vo.d.ts +3 -0
- package/dist/week-iso-id.vo.js +4 -4
- package/dist/week.vo.js +1 -1
- package/dist/weekday.vo.d.ts +7 -6
- package/dist/weekday.vo.js +20 -13
- package/dist/weight.vo.d.ts +12 -0
- package/dist/weight.vo.js +37 -27
- package/dist/year-iso-id.vo.d.ts +3 -0
- package/dist/year-iso-id.vo.js +4 -6
- package/dist/year.vo.d.ts +2 -0
- package/dist/year.vo.js +4 -2
- package/package.json +1 -1
- package/readme.md +0 -1
- package/src/basename.vo.ts +25 -14
- package/src/clock.vo.ts +1 -0
- package/src/date-calculator.service.ts +10 -15
- package/src/date-range.vo.ts +3 -1
- package/src/day-iso-id.vo.ts +9 -10
- package/src/day.vo.ts +17 -10
- package/src/directory-path-absolute.vo.ts +12 -5
- package/src/directory-path-relative.vo.ts +13 -6
- package/src/dll.service.ts +45 -43
- package/src/extension.vo.ts +14 -12
- package/src/file-path-absolute-schema.vo.ts +15 -6
- package/src/file-path-relative-schema.vo.ts +13 -6
- package/src/file-path.vo.ts +15 -11
- package/src/filename-from-string.vo.ts +20 -15
- package/src/filename-suffix.vo.ts +8 -4
- package/src/filename.vo.ts +14 -15
- package/src/height.vo.ts +71 -53
- package/src/index.ts +0 -1
- package/src/mime.vo.ts +10 -7
- package/src/month-iso-id.vo.ts +10 -20
- package/src/month.vo.ts +19 -13
- package/src/object-key.vo.ts +21 -7
- package/src/outlier-detector.service.ts +1 -0
- package/src/package-version.vo.ts +18 -47
- package/src/pagination.service.ts +15 -13
- package/src/quarter-iso-id.vo.ts +11 -13
- package/src/quarter.vo.ts +3 -0
- package/src/rate-limiter.service.ts +7 -7
- package/src/reordering.service.ts +52 -38
- package/src/revision.vo.ts +17 -8
- package/src/rounding.adapter.ts +1 -3
- package/src/size.vo.ts +6 -16
- package/src/streak-calculator.service.ts +12 -17
- package/src/time-zone-offset-value.vo.ts +2 -7
- package/src/time.service.ts +43 -45
- package/src/timezone.vo.ts +1 -3
- package/src/week-iso-id.vo.ts +13 -14
- package/src/week.vo.ts +4 -2
- package/src/weekday.vo.ts +27 -13
- package/src/weight.vo.ts +49 -30
- package/src/year-iso-id.vo.ts +6 -9
- package/src/year.vo.ts +12 -2
- package/dist/stepper.service.d.ts +0 -23
- package/dist/stepper.service.js +0 -33
- package/src/stepper.service.ts +0 -43
package/dist/weight.vo.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import { RoundToDecimal } from "./rounding.adapter";
|
|
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
3
|
export var WeightUnit;
|
|
10
4
|
(function (WeightUnit) {
|
|
11
5
|
WeightUnit["kg"] = "kg";
|
|
12
6
|
WeightUnit["lb"] = "lb";
|
|
13
7
|
})(WeightUnit || (WeightUnit = {}));
|
|
8
|
+
export const WeightNonFiniteError = { error: "weight.non_finite" };
|
|
9
|
+
export const WeightNegativeError = { error: "weight.negative" };
|
|
10
|
+
export const WeightNonPositiveError = { error: "weight.non_positive" };
|
|
11
|
+
export const WeightGramsNonNegativeError = { error: "weight.grams_non_negative" };
|
|
12
|
+
const WeightQuantityNumber = z
|
|
13
|
+
.number(WeightNonFiniteError)
|
|
14
|
+
.refine(Number.isFinite, WeightNonFiniteError)
|
|
15
|
+
.min(0, WeightNegativeError);
|
|
16
|
+
const DivisionScalarNumber = z
|
|
17
|
+
.number(WeightNonFiniteError)
|
|
18
|
+
.refine(Number.isFinite, WeightNonFiniteError)
|
|
19
|
+
.gt(0, WeightNonPositiveError);
|
|
20
|
+
const CanonicalGramsInteger = z
|
|
21
|
+
.number(WeightGramsNonNegativeError)
|
|
22
|
+
.int(WeightGramsNonNegativeError)
|
|
23
|
+
.min(0, WeightGramsNonNegativeError);
|
|
14
24
|
export class Weight {
|
|
15
25
|
grams;
|
|
16
26
|
static GRAMS_PER_KILOGRAM = 1_000;
|
|
@@ -20,22 +30,22 @@ export class Weight {
|
|
|
20
30
|
this.grams = grams;
|
|
21
31
|
}
|
|
22
32
|
static fromKilograms(kilograms) {
|
|
23
|
-
|
|
24
|
-
const gramsRounded = Math.round(
|
|
25
|
-
|
|
26
|
-
return new Weight(
|
|
33
|
+
const kilogramsParsed = WeightQuantityNumber.parse(kilograms);
|
|
34
|
+
const gramsRounded = Math.round(kilogramsParsed * Weight.GRAMS_PER_KILOGRAM);
|
|
35
|
+
const grams = CanonicalGramsInteger.parse(gramsRounded);
|
|
36
|
+
return new Weight(grams);
|
|
27
37
|
}
|
|
28
38
|
static fromPounds(pounds) {
|
|
29
|
-
|
|
30
|
-
const gramsRounded = Math.round(
|
|
31
|
-
|
|
32
|
-
return new Weight(
|
|
39
|
+
const poundsParsed = WeightQuantityNumber.parse(pounds);
|
|
40
|
+
const gramsRounded = Math.round(poundsParsed * Weight.KILOGRAMS_PER_POUND * Weight.GRAMS_PER_KILOGRAM);
|
|
41
|
+
const grams = CanonicalGramsInteger.parse(gramsRounded);
|
|
42
|
+
return new Weight(grams);
|
|
33
43
|
}
|
|
34
44
|
static fromGrams(grams) {
|
|
35
|
-
|
|
36
|
-
const gramsRounded = Math.round(
|
|
37
|
-
|
|
38
|
-
return new Weight(
|
|
45
|
+
const gramsParsed = WeightQuantityNumber.parse(grams);
|
|
46
|
+
const gramsRounded = Math.round(gramsParsed);
|
|
47
|
+
const integerGrams = CanonicalGramsInteger.parse(gramsRounded);
|
|
48
|
+
return new Weight(integerGrams);
|
|
39
49
|
}
|
|
40
50
|
static zero() {
|
|
41
51
|
return new Weight(0);
|
|
@@ -52,7 +62,7 @@ export class Weight {
|
|
|
52
62
|
return rounding ? rounding.round(pounds) : pounds;
|
|
53
63
|
}
|
|
54
64
|
format(unit, rounding = new RoundToDecimal(2)) {
|
|
55
|
-
const value =
|
|
65
|
+
const value = unit === WeightUnit.kg ? this.toKilograms(rounding) : this.toPounds(rounding);
|
|
56
66
|
return `${value.toString()} ${unit}`;
|
|
57
67
|
}
|
|
58
68
|
add(other) {
|
|
@@ -63,16 +73,16 @@ export class Weight {
|
|
|
63
73
|
return new Weight(result < 0 ? 0 : result);
|
|
64
74
|
}
|
|
65
75
|
multiply(factor) {
|
|
66
|
-
|
|
67
|
-
const gramsRounded = Math.round(this.grams *
|
|
68
|
-
|
|
69
|
-
return new Weight(
|
|
76
|
+
const factorParsed = WeightQuantityNumber.parse(factor);
|
|
77
|
+
const gramsRounded = Math.round(this.grams * factorParsed);
|
|
78
|
+
const grams = CanonicalGramsInteger.parse(gramsRounded);
|
|
79
|
+
return new Weight(grams);
|
|
70
80
|
}
|
|
71
81
|
divideByScalar(divisor) {
|
|
72
|
-
|
|
73
|
-
const gramsRounded = Math.round(this.grams /
|
|
74
|
-
|
|
75
|
-
return new Weight(
|
|
82
|
+
const divisorParsed = DivisionScalarNumber.parse(divisor);
|
|
83
|
+
const gramsRounded = Math.round(this.grams / divisorParsed);
|
|
84
|
+
const grams = CanonicalGramsInteger.parse(gramsRounded);
|
|
85
|
+
return new Weight(grams);
|
|
76
86
|
}
|
|
77
87
|
equals(other) {
|
|
78
88
|
return this.grams === other.grams;
|
package/dist/year-iso-id.vo.d.ts
CHANGED
package/dist/year-iso-id.vo.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
|
+
export const YearIsoIdError = { error: "year-iso-id.invalid" };
|
|
2
3
|
export const YearIsoId = z
|
|
3
|
-
.string()
|
|
4
|
-
.regex(/^\d{4}
|
|
5
|
-
.refine((value) =>
|
|
6
|
-
const year = Number(value);
|
|
7
|
-
return value.length === 4 && Number.isInteger(year);
|
|
8
|
-
}, { message: "year-iso-id.invalid" });
|
|
4
|
+
.string(YearIsoIdError)
|
|
5
|
+
.regex(/^\d{4}$/, YearIsoIdError)
|
|
6
|
+
.refine((value) => Number.isInteger(Number(value)), YearIsoIdError);
|
package/dist/year.vo.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DateRange } from "./date-range.vo";
|
|
2
2
|
import { type TimestampType } from "./timestamp.vo";
|
|
3
3
|
import { type YearIsoIdType } from "./year-iso-id.vo";
|
|
4
|
+
export declare const YearInvalidIntegerError: "year.invalid_integer";
|
|
5
|
+
export declare const YearOutOfRangeError: "year.out_of_range";
|
|
4
6
|
export declare class Year extends DateRange {
|
|
5
7
|
toIsoId(): YearIsoIdType;
|
|
6
8
|
isLeapYear(): boolean;
|
package/dist/year.vo.js
CHANGED
|
@@ -2,6 +2,8 @@ import { addYears, endOfYear, getYear, startOfYear } from "date-fns";
|
|
|
2
2
|
import { DateRange } from "./date-range.vo";
|
|
3
3
|
import { Timestamp } from "./timestamp.vo";
|
|
4
4
|
import { YearIsoId } from "./year-iso-id.vo";
|
|
5
|
+
export const YearInvalidIntegerError = "year.invalid_integer";
|
|
6
|
+
export const YearOutOfRangeError = "year.out_of_range";
|
|
5
7
|
export class Year extends DateRange {
|
|
6
8
|
toIsoId() {
|
|
7
9
|
return String(getYear(this.getStart()));
|
|
@@ -32,9 +34,9 @@ export class Year extends DateRange {
|
|
|
32
34
|
}
|
|
33
35
|
static fromNumber(value) {
|
|
34
36
|
if (!Number.isInteger(value))
|
|
35
|
-
throw new Error(
|
|
37
|
+
throw new Error(YearInvalidIntegerError);
|
|
36
38
|
if (value < 0 || value > 9999)
|
|
37
|
-
throw new Error(
|
|
39
|
+
throw new Error(YearOutOfRangeError);
|
|
38
40
|
const reference = Timestamp.parse(Date.UTC(value, 0, 1, 0, 0, 0, 0));
|
|
39
41
|
return Year.fromTimestamp(reference);
|
|
40
42
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
package/src/basename.vo.ts
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
3
|
+
export const BasenameTypeError = "basename.not.string" as const;
|
|
4
|
+
export const BasenameEmptyError = "basename.empty" as const;
|
|
5
|
+
export const BasenameTooLongError = "basename.too.long" as const;
|
|
6
|
+
export const BasenameSlashesForbiddenError = "basename.slashes.forbidden" as const;
|
|
7
|
+
export const BasenameControlCharsForbiddenError = "basename.control.chars.forbidden" as const;
|
|
8
|
+
export const BasenameDotSegmentsForbiddenError = "basename.dot.segments.forbidden" as const;
|
|
9
|
+
export const BasenameDotfilesForbiddenError = "basename.dotfiles.forbidden" as const;
|
|
10
|
+
export const BasenameTrailingDotForbiddenError = "basename.trailing.dot.forbidden" as const;
|
|
11
|
+
export const BasenameBadCharsError = "basename.bad.chars" as const;
|
|
12
|
+
|
|
13
|
+
export const Basename = z
|
|
14
|
+
.string(BasenameTypeError)
|
|
5
15
|
.trim()
|
|
6
|
-
.min(1,
|
|
7
|
-
.max(128,
|
|
8
|
-
.refine((s) => !/[/\\]/.test(s),
|
|
16
|
+
.min(1, BasenameEmptyError)
|
|
17
|
+
.max(128, BasenameTooLongError)
|
|
18
|
+
.refine((s) => !/[/\\]/.test(s), BasenameSlashesForbiddenError)
|
|
19
|
+
// dot-related checks: dot-segments first for specific errors…
|
|
9
20
|
// biome-ignore lint: lint/suspicious/noControlCharactersInRegex
|
|
10
|
-
.refine((
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.refine((
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
export type BasenameType = z.infer<typeof
|
|
21
|
+
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), BasenameControlCharsForbiddenError)
|
|
22
|
+
.refine((value) => value !== "." && value !== "..", BasenameDotSegmentsForbiddenError)
|
|
23
|
+
// …then any other dotfile
|
|
24
|
+
.refine((value) => !value.startsWith("."), BasenameDotfilesForbiddenError)
|
|
25
|
+
.refine((value) => !value.endsWith("."), BasenameTrailingDotForbiddenError)
|
|
26
|
+
.regex(/^[A-Za-z0-9._-]+$/, BasenameBadCharsError)
|
|
27
|
+
.brand("Basename");
|
|
28
|
+
|
|
29
|
+
export type BasenameType = z.infer<typeof Basename>;
|
package/src/clock.vo.ts
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { Time } from "./time.service";
|
|
2
|
-
import type
|
|
2
|
+
import { Timestamp, type TimestampType } from "./timestamp.vo";
|
|
3
3
|
|
|
4
4
|
type GetStartOfDayTsInTzConfigType = { now: TimestampType; timeZoneOffsetMs: number };
|
|
5
5
|
|
|
6
6
|
export class DateCalculator {
|
|
7
|
-
static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType) {
|
|
8
|
-
const
|
|
9
|
-
startOfDayUTC.setUTCHours(0, 0, 0, 0);
|
|
7
|
+
static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType): TimestampType {
|
|
8
|
+
const dayMs = Time.Days(1).ms;
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
// UTC midnight for the UTC date of `now`
|
|
11
|
+
const utcMidnight = Math.floor(config.now / dayMs) * dayMs;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Candidate start of the local day (in UTC), anchored to the same UTC date
|
|
14
|
+
let start = utcMidnight + config.timeZoneOffsetMs;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
16
|
+
// If the candidate is in the future relative to `now`, it means local midnight was "yesterday" in UTC.
|
|
17
|
+
if (start > config.now) start -= dayMs;
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay - Time.Days(1).ms;
|
|
19
|
+
return Timestamp.parse(start);
|
|
25
20
|
}
|
|
26
21
|
}
|
package/src/date-range.vo.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { TimestampType } from "./timestamp.vo";
|
|
2
2
|
|
|
3
|
+
export const DateRangeInvalidError = "invalid.date.range" as const;
|
|
4
|
+
|
|
3
5
|
export class DateRange {
|
|
4
6
|
constructor(
|
|
5
7
|
private readonly start: TimestampType,
|
|
6
8
|
private readonly end: TimestampType,
|
|
7
9
|
) {
|
|
8
|
-
if (start > end) throw new Error(
|
|
10
|
+
if (start > end) throw new Error(DateRangeInvalidError);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
getStart(): TimestampType {
|
package/src/day-iso-id.vo.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { isValid, parseISO } from "date-fns";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
3
|
|
|
4
|
+
export const DayIsoIdError = { error: "invalid.day.iso.id" } as const;
|
|
5
|
+
|
|
4
6
|
export const DayIsoId = z
|
|
5
|
-
.string()
|
|
6
|
-
|
|
7
|
-
.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
},
|
|
13
|
-
{ message: "day-iso-id.invalid" },
|
|
14
|
-
);
|
|
7
|
+
.string(DayIsoIdError)
|
|
8
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, DayIsoIdError)
|
|
9
|
+
.refine((value) => {
|
|
10
|
+
const date = parseISO(value);
|
|
11
|
+
|
|
12
|
+
return isValid(date) && value === date.toISOString().slice(0, 10);
|
|
13
|
+
}, DayIsoIdError);
|
|
15
14
|
|
|
16
15
|
export type DayIsoIdType = z.infer<typeof DayIsoId>;
|
package/src/day.vo.ts
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, type DayIsoIdType } from "./day-iso-id.vo";
|
|
4
3
|
import { Time } from "./time.service";
|
|
@@ -10,28 +9,35 @@ export class Day extends DateRange {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
toIsoId(): DayIsoIdType {
|
|
13
|
-
|
|
12
|
+
const midday = this.getStart() + Time.Hours(12).ms;
|
|
13
|
+
|
|
14
|
+
return new Date(midday).toISOString().slice(0, 10) as DayIsoIdType;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
previous(): Day {
|
|
17
|
-
const shifted =
|
|
18
|
+
const shifted = this.getStart() - Time.Days(1).ms;
|
|
19
|
+
|
|
18
20
|
return Day.fromTimestamp(Timestamp.parse(shifted));
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
next(): Day {
|
|
22
|
-
const shifted =
|
|
24
|
+
const shifted = this.getStart() + Time.Days(1).ms;
|
|
25
|
+
|
|
23
26
|
return Day.fromTimestamp(Timestamp.parse(shifted));
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
shift(count: number): Day {
|
|
27
|
-
const shifted =
|
|
30
|
+
const shifted = this.getStart() + count * Time.Days(1).ms;
|
|
31
|
+
|
|
28
32
|
return Day.fromTimestamp(Timestamp.parse(shifted));
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
static fromTimestamp(timestamp: TimestampType): Day {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
36
|
+
const date = new Date(timestamp);
|
|
37
|
+
const startUtc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
38
|
+
const endUtc = startUtc + Time.Days(1).ms - 1;
|
|
39
|
+
|
|
40
|
+
return new Day(Timestamp.parse(startUtc), Timestamp.parse(endUtc));
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
static fromNow(now: TimestampType): Day {
|
|
@@ -40,8 +46,9 @@ export class Day extends DateRange {
|
|
|
40
46
|
|
|
41
47
|
static fromIsoId(isoId: DayIsoIdType): Day {
|
|
42
48
|
const [year, month, day] = DayIsoId.parse(isoId).split("-").map(Number);
|
|
49
|
+
const startUtc = Date.UTC(year, month - 1, day);
|
|
50
|
+
const endUtc = startUtc + Time.Days(1).ms - 1;
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
return Day.fromTimestamp(Timestamp.parse(reference.getTime()));
|
|
52
|
+
return new Day(Timestamp.parse(startUtc), Timestamp.parse(endUtc));
|
|
46
53
|
}
|
|
47
54
|
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
+
export const AbsDirTypeError = "abs_dir.not.string" as const;
|
|
4
|
+
export const AbsDirMustStartWithSlashError = "abs_dir_must_start_with_slash" as const;
|
|
5
|
+
export const AbsDirBackslashForbiddenError = "abs_dir_backslash_forbidden" as const;
|
|
6
|
+
export const AbsDirControlCharsForbiddenError = "abs_dir_control_chars_forbidden" as const;
|
|
7
|
+
export const AbsDirBadSegmentsError = "abs_dir_bad_segments" as const;
|
|
8
|
+
|
|
3
9
|
export const DirectoryPathAbsoluteSchema = z
|
|
4
|
-
.string()
|
|
10
|
+
.string(AbsDirTypeError)
|
|
5
11
|
.trim()
|
|
6
|
-
.refine((value) => value.startsWith("/"),
|
|
7
|
-
.refine((value) => !value.includes("\\"),
|
|
12
|
+
.refine((value) => value.startsWith("/"), AbsDirMustStartWithSlashError)
|
|
13
|
+
.refine((value) => !value.includes("\\"), AbsDirBackslashForbiddenError)
|
|
8
14
|
// biome-ignore lint: lint/suspicious/noControlCharactersInRegex
|
|
9
|
-
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value),
|
|
15
|
+
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), AbsDirControlCharsForbiddenError)
|
|
16
|
+
// collapse duplicate slashes, then drop trailing slash unless it's the root "/"
|
|
10
17
|
.transform((value) => value.replace(/\/{2,}/g, "/"))
|
|
11
18
|
.transform((value) => (value !== "/" && value.endsWith("/") ? value.slice(0, -1) : value))
|
|
12
19
|
.refine((value) => {
|
|
@@ -16,7 +23,7 @@ export const DirectoryPathAbsoluteSchema = z
|
|
|
16
23
|
(segment) =>
|
|
17
24
|
segment.length > 0 && /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== "..",
|
|
18
25
|
);
|
|
19
|
-
},
|
|
26
|
+
}, AbsDirBadSegmentsError)
|
|
20
27
|
.brand("directory_path_absolute");
|
|
21
28
|
|
|
22
29
|
export type DirectoryPathAbsoluteType = z.infer<typeof DirectoryPathAbsoluteSchema>;
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
+
export const RelDirTypeError = "rel_dir.not.string" as const;
|
|
4
|
+
export const RelDirMustNotStartWithSlashError = "rel_dir_must_not_start_with_slash" as const;
|
|
5
|
+
export const RelDirBackslashForbiddenError = "rel_dir_backslash_forbidden" as const;
|
|
6
|
+
export const RelDirControlCharsForbiddenError = "rel_dir_control_chars_forbidden" as const;
|
|
7
|
+
export const RelDirEmptyError = "rel_dir_empty" as const;
|
|
8
|
+
export const RelDirBadSegmentsError = "rel_dir_bad_segments" as const;
|
|
9
|
+
|
|
3
10
|
export const DirectoryPathRelativeSchema = z
|
|
4
|
-
.string()
|
|
11
|
+
.string(RelDirTypeError)
|
|
5
12
|
.trim()
|
|
6
|
-
.refine((value) => !value.startsWith("/"),
|
|
7
|
-
.refine((value) => !value.includes("\\"),
|
|
13
|
+
.refine((value) => !value.startsWith("/"), RelDirMustNotStartWithSlashError)
|
|
14
|
+
.refine((value) => !value.includes("\\"), RelDirBackslashForbiddenError)
|
|
8
15
|
// biome-ignore lint: lint/suspicious/noControlCharactersInRegex
|
|
9
|
-
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value),
|
|
16
|
+
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), RelDirControlCharsForbiddenError)
|
|
10
17
|
.transform((value) => value.replace(/\/{2,}/g, "/"))
|
|
11
18
|
.transform((value) => value.replace(/^\/+|\/+$/g, ""))
|
|
12
|
-
.refine((value) => value.length > 0,
|
|
19
|
+
.refine((value) => value.length > 0, RelDirEmptyError)
|
|
13
20
|
.refine(
|
|
14
21
|
(value) =>
|
|
15
22
|
value
|
|
16
23
|
.split("/")
|
|
17
24
|
.every((segment) => /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== ".."),
|
|
18
|
-
|
|
25
|
+
RelDirBadSegmentsError,
|
|
19
26
|
)
|
|
20
27
|
.brand("directory_path_relative");
|
|
21
28
|
|
package/src/dll.service.ts
CHANGED
|
@@ -9,13 +9,11 @@ export class Node<T> {
|
|
|
9
9
|
|
|
10
10
|
forward(n: number): Node<T> | null {
|
|
11
11
|
let currentNode: Node<T> | null = this;
|
|
12
|
+
let steps = n;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
if (currentNode === null) {
|
|
15
|
-
return currentNode;
|
|
16
|
-
}
|
|
17
|
-
|
|
14
|
+
while (steps > 0 && currentNode) {
|
|
18
15
|
currentNode = currentNode.next;
|
|
16
|
+
steps -= 1;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
return currentNode;
|
|
@@ -23,14 +21,13 @@ export class Node<T> {
|
|
|
23
21
|
|
|
24
22
|
backward(n: number): Node<T> | null {
|
|
25
23
|
let currentNode: Node<T> | null = this;
|
|
24
|
+
let steps = n;
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
if (currentNode === null) {
|
|
29
|
-
return currentNode;
|
|
30
|
-
}
|
|
31
|
-
|
|
26
|
+
while (steps > 0 && currentNode) {
|
|
32
27
|
currentNode = currentNode.prev;
|
|
28
|
+
steps -= 1;
|
|
33
29
|
}
|
|
30
|
+
|
|
34
31
|
return currentNode;
|
|
35
32
|
}
|
|
36
33
|
}
|
|
@@ -58,39 +55,31 @@ export class DoublyLinkedList<T> {
|
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
append(node: Node<T>): void {
|
|
61
|
-
if (this.
|
|
62
|
-
this.size++;
|
|
63
|
-
|
|
58
|
+
if (this.tail === null) {
|
|
64
59
|
this.head = node;
|
|
65
60
|
this.tail = node;
|
|
66
61
|
} else {
|
|
67
|
-
this.size++;
|
|
68
|
-
|
|
69
62
|
this.tail.next = node;
|
|
70
63
|
node.prev = this.tail;
|
|
71
|
-
|
|
72
64
|
this.tail = node;
|
|
73
65
|
}
|
|
66
|
+
this.size += 1;
|
|
74
67
|
}
|
|
75
68
|
|
|
76
69
|
prepend(node: Node<T>): void {
|
|
77
|
-
if (this.head === null
|
|
78
|
-
this.size++;
|
|
79
|
-
|
|
70
|
+
if (this.head === null) {
|
|
80
71
|
this.head = node;
|
|
81
72
|
this.tail = node;
|
|
82
73
|
} else {
|
|
83
|
-
this.size++;
|
|
84
|
-
|
|
85
74
|
node.next = this.head;
|
|
86
75
|
this.head.prev = node;
|
|
87
76
|
this.head = node;
|
|
88
77
|
}
|
|
78
|
+
this.size += 1;
|
|
89
79
|
}
|
|
90
80
|
|
|
91
81
|
clear(): void {
|
|
92
82
|
this.size = 0;
|
|
93
|
-
|
|
94
83
|
this.head = null;
|
|
95
84
|
this.tail = null;
|
|
96
85
|
}
|
|
@@ -108,7 +97,7 @@ export class DoublyLinkedList<T> {
|
|
|
108
97
|
this.tail = node.prev;
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
this.size
|
|
100
|
+
this.size -= 1;
|
|
112
101
|
node.prev = null;
|
|
113
102
|
node.next = null;
|
|
114
103
|
}
|
|
@@ -116,40 +105,57 @@ export class DoublyLinkedList<T> {
|
|
|
116
105
|
insertAfter(node: Node<T>, target: Node<T>): void {
|
|
117
106
|
if (target === this.tail) {
|
|
118
107
|
this.append(node);
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nextNode = target.next;
|
|
112
|
+
this.size += 1;
|
|
113
|
+
|
|
114
|
+
node.prev = target;
|
|
115
|
+
node.next = nextNode;
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// biome-ignore lint: lint/style/noNonNullAssertion
|
|
125
|
-
target.next!.prev = node;
|
|
126
|
-
target.next = node;
|
|
117
|
+
if (nextNode) {
|
|
118
|
+
nextNode.prev = node;
|
|
127
119
|
}
|
|
120
|
+
|
|
121
|
+
target.next = node;
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
insertBefore(node: Node<T>, target: Node<T>): void {
|
|
131
125
|
if (target === this.head) {
|
|
132
126
|
this.prepend(node);
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const prevNode = target.prev;
|
|
131
|
+
this.size += 1;
|
|
135
132
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
node.next = target;
|
|
134
|
+
node.prev = prevNode;
|
|
135
|
+
|
|
136
|
+
if (prevNode) {
|
|
137
|
+
prevNode.next = node;
|
|
141
138
|
}
|
|
139
|
+
|
|
140
|
+
target.prev = node;
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
find(callback: (node: Node<T>) => boolean): Node<T> | null {
|
|
145
|
-
|
|
144
|
+
let current = this.head;
|
|
145
|
+
while (current) {
|
|
146
|
+
if (callback(current)) return current;
|
|
147
|
+
current = current.next;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
reverse(): void {
|
|
149
153
|
[this.head, this.tail] = [this.tail, this.head];
|
|
150
154
|
|
|
151
155
|
for (const node of this) {
|
|
152
|
-
|
|
156
|
+
const originalNext = node.next;
|
|
157
|
+
node.next = node.prev;
|
|
158
|
+
node.prev = originalNext;
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
161
|
|
|
@@ -159,20 +165,16 @@ export class DoublyLinkedList<T> {
|
|
|
159
165
|
|
|
160
166
|
static fromArray<T>(array: T[]): DoublyLinkedList<T> {
|
|
161
167
|
const dll = new DoublyLinkedList<T>();
|
|
162
|
-
|
|
163
168
|
for (const item of array) {
|
|
164
169
|
dll.append(new Node<T>(item));
|
|
165
170
|
}
|
|
166
|
-
|
|
167
171
|
return dll;
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
*[Symbol.iterator](): IterableIterator<Node<T>> {
|
|
171
175
|
let current: Node<T> | null = this.head;
|
|
172
|
-
|
|
173
176
|
while (current) {
|
|
174
177
|
yield current;
|
|
175
|
-
|
|
176
178
|
current = current.next;
|
|
177
179
|
}
|
|
178
180
|
}
|
package/src/extension.vo.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
3
|
+
export const ExtensionTypeError = "extension.not.string" as const;
|
|
4
|
+
export const ExtensionEmptyError = "extension.empty" as const;
|
|
5
|
+
export const ExtensionTooLongError = "extension.too.long" as const;
|
|
6
|
+
export const ExtensionBadCharsError = "extension.bad.chars" as const;
|
|
7
|
+
|
|
8
|
+
export const Extension = z
|
|
9
|
+
.string(ExtensionTypeError)
|
|
5
10
|
.trim()
|
|
11
|
+
.toLowerCase()
|
|
6
12
|
.transform((value) => (value.startsWith(".") ? value.slice(1) : value))
|
|
7
|
-
.
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.regex(/^[a-z0-9]+$/, "extension_bad_chars"),
|
|
14
|
-
)
|
|
15
|
-
.brand("extension");
|
|
16
|
-
export type ExtensionType = z.infer<typeof ExtensionSchema>;
|
|
13
|
+
.refine((value) => value.length >= 1, ExtensionEmptyError)
|
|
14
|
+
.refine((value) => value.length <= 16, ExtensionTooLongError)
|
|
15
|
+
.refine((value) => /^[a-z0-9]+$/.test(value), ExtensionBadCharsError)
|
|
16
|
+
.brand("Extension");
|
|
17
|
+
|
|
18
|
+
export type ExtensionType = z.infer<typeof Extension>;
|