@bgord/tools 0.2.2 → 0.3.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.
- package/dist/date-calculator.service.d.ts +9 -0
- package/dist/date-calculator.service.js +16 -0
- package/dist/date-formatter.service.d.ts +10 -0
- package/dist/date-formatter.service.js +15 -0
- package/dist/dates-of-the-week.vo.d.ts +9 -0
- package/dist/dates-of-the-week.vo.js +10 -0
- package/dist/etags.vo.d.ts +22 -0
- package/dist/etags.vo.js +34 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +14 -1
- package/dist/mime.vo.d.ts +18 -0
- package/dist/mime.vo.js +35 -0
- package/dist/rate-limiter.service.d.ts +19 -0
- package/dist/rate-limiter.service.js +21 -0
- package/dist/relative-date.vo.d.ts +12 -0
- package/dist/relative-date.vo.js +17 -0
- package/dist/size.vo.d.ts +30 -0
- package/dist/size.vo.js +67 -0
- package/dist/sleep.service.d.ts +3 -0
- package/dist/sleep.service.js +3 -0
- package/dist/stopwatch.service.d.ts +11 -0
- package/dist/stopwatch.service.js +19 -0
- package/dist/time.service.d.ts +21 -0
- package/dist/time.service.js +44 -0
- package/dist/timestamp.vo.d.ts +3 -0
- package/dist/timestamp.vo.js +6 -0
- package/dist/ts-utils.d.ts +3 -0
- package/dist/ts-utils.js +1 -0
- package/package.json +8 -4
- package/src/date-calculator.service.ts +29 -0
- package/src/date-formatter.service.ts +23 -0
- package/src/dates-of-the-week.vo.ts +9 -0
- package/src/etags.vo.ts +51 -0
- package/src/index.ts +14 -1
- package/src/mime.vo.ts +50 -0
- package/src/rate-limiter.service.ts +42 -0
- package/src/relative-date.vo.ts +24 -0
- package/src/size.vo.ts +94 -0
- package/src/sleep.service.ts +3 -0
- package/src/stopwatch.service.ts +28 -0
- package/src/time.service.ts +96 -0
- package/src/timestamp.vo.ts +9 -0
- package/src/ts-utils.ts +5 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
2
|
+
type GetStartOfDayTsInTzConfigType = {
|
|
3
|
+
now: TimestampType;
|
|
4
|
+
timeZoneOffsetMs: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class DateCalculator {
|
|
7
|
+
static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType): number;
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Time } from "./time.service";
|
|
2
|
+
export class DateCalculator {
|
|
3
|
+
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;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { format } from "date-fns";
|
|
2
|
+
type FormattedDateType = string;
|
|
3
|
+
type DateFormattersInputType = Parameters<typeof format>[0];
|
|
4
|
+
export declare class DateFormatters {
|
|
5
|
+
static datetime(date: DateFormattersInputType): FormattedDateType;
|
|
6
|
+
static date(date: DateFormattersInputType): FormattedDateType;
|
|
7
|
+
static monthDay(date: DateFormattersInputType): FormattedDateType;
|
|
8
|
+
static relative(date: DateFormattersInputType): string;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { format, formatDistanceToNow } from "date-fns";
|
|
2
|
+
export class DateFormatters {
|
|
3
|
+
static datetime(date) {
|
|
4
|
+
return format(date, "yyyy/MM/dd HH:mm");
|
|
5
|
+
}
|
|
6
|
+
static date(date) {
|
|
7
|
+
return format(date, "yyyy/MM/dd");
|
|
8
|
+
}
|
|
9
|
+
static monthDay(date) {
|
|
10
|
+
return format(date, "MM/dd");
|
|
11
|
+
}
|
|
12
|
+
static relative(date) {
|
|
13
|
+
return formatDistanceToNow(date, { addSuffix: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export var DayOfTheWeekEnum;
|
|
2
|
+
(function (DayOfTheWeekEnum) {
|
|
3
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Monday"] = 1] = "Monday";
|
|
4
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Tuesday"] = 2] = "Tuesday";
|
|
5
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Wednesday"] = 3] = "Wednesday";
|
|
6
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Thursday"] = 4] = "Thursday";
|
|
7
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Friday"] = 5] = "Friday";
|
|
8
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Saturday"] = 6] = "Saturday";
|
|
9
|
+
DayOfTheWeekEnum[DayOfTheWeekEnum["Sunday"] = 0] = "Sunday";
|
|
10
|
+
})(DayOfTheWeekEnum || (DayOfTheWeekEnum = {}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
declare const RevisionValueSchema: z.ZodNumber;
|
|
3
|
+
type RevisionValueSchemaType = z.infer<typeof RevisionValueSchema>;
|
|
4
|
+
type ETagValueType = string;
|
|
5
|
+
export declare class ETag {
|
|
6
|
+
readonly revision: RevisionValueSchemaType;
|
|
7
|
+
static HEADER_NAME: string;
|
|
8
|
+
static IF_MATCH_HEADER_NAME: string;
|
|
9
|
+
readonly value: ETagValueType;
|
|
10
|
+
private constructor();
|
|
11
|
+
static fromHeader(value?: ETagValueType): ETag | null;
|
|
12
|
+
}
|
|
13
|
+
export type WeakETagValueType = string;
|
|
14
|
+
export declare class WeakETag {
|
|
15
|
+
readonly revision: RevisionValueSchemaType;
|
|
16
|
+
static HEADER_NAME: string;
|
|
17
|
+
static IF_MATCH_HEADER_NAME: string;
|
|
18
|
+
readonly value: WeakETagValueType;
|
|
19
|
+
private constructor();
|
|
20
|
+
static fromHeader(value?: WeakETagValueType): WeakETag | null;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
package/dist/etags.vo.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
const RevisionValueSchema = z.number().int().min(0);
|
|
3
|
+
export class ETag {
|
|
4
|
+
constructor(revision) {
|
|
5
|
+
this.revision = revision;
|
|
6
|
+
this.value = revision.toString();
|
|
7
|
+
}
|
|
8
|
+
static fromHeader(value) {
|
|
9
|
+
if (value?.startsWith("W/"))
|
|
10
|
+
return null;
|
|
11
|
+
const candidate = Number(value);
|
|
12
|
+
if (Number.isNaN(candidate))
|
|
13
|
+
return null;
|
|
14
|
+
return new ETag(candidate);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
ETag.HEADER_NAME = "ETag";
|
|
18
|
+
ETag.IF_MATCH_HEADER_NAME = "if-match";
|
|
19
|
+
export class WeakETag {
|
|
20
|
+
constructor(revision) {
|
|
21
|
+
this.revision = revision;
|
|
22
|
+
this.value = `W/${revision.toString()}`;
|
|
23
|
+
}
|
|
24
|
+
static fromHeader(value) {
|
|
25
|
+
if (!value?.startsWith("W/"))
|
|
26
|
+
throw Error("Invalid WeakETag");
|
|
27
|
+
const candidate = Number(value.split("W/")[1]);
|
|
28
|
+
if (Number.isNaN(candidate))
|
|
29
|
+
return null;
|
|
30
|
+
return new WeakETag(candidate);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
WeakETag.HEADER_NAME = "ETag";
|
|
34
|
+
WeakETag.IF_MATCH_HEADER_NAME = "if-match";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./date-calculator.service";
|
|
2
|
+
export * from "./date-formatter.service";
|
|
3
|
+
export * from "./dates-of-the-week.vo";
|
|
4
|
+
export * from "./etags.vo";
|
|
5
|
+
export * from "./mime.vo";
|
|
2
6
|
export * from "./noop.service";
|
|
7
|
+
export * from "./rate-limiter.service";
|
|
8
|
+
export * from "./relative-date.vo";
|
|
9
|
+
export * from "./rounding.service";
|
|
10
|
+
export * from "./size.vo";
|
|
11
|
+
export * from "./sleep.service";
|
|
12
|
+
export * from "./stopwatch.service";
|
|
13
|
+
export * from "./time.service";
|
|
14
|
+
export * from "./timestamp.vo";
|
|
15
|
+
export * from "./ts-utils";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./date-calculator.service";
|
|
2
|
+
export * from "./date-formatter.service";
|
|
3
|
+
export * from "./dates-of-the-week.vo";
|
|
4
|
+
export * from "./etags.vo";
|
|
5
|
+
export * from "./mime.vo";
|
|
2
6
|
export * from "./noop.service";
|
|
7
|
+
export * from "./rate-limiter.service";
|
|
8
|
+
export * from "./relative-date.vo";
|
|
9
|
+
export * from "./rounding.service";
|
|
10
|
+
export * from "./size.vo";
|
|
11
|
+
export * from "./sleep.service";
|
|
12
|
+
export * from "./stopwatch.service";
|
|
13
|
+
export * from "./time.service";
|
|
14
|
+
export * from "./timestamp.vo";
|
|
15
|
+
export * from "./ts-utils";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type MimeRawType = string;
|
|
2
|
+
type MimeTypeType = string;
|
|
3
|
+
type MimeSubtypeType = string;
|
|
4
|
+
export declare class Mime {
|
|
5
|
+
readonly raw: MimeRawType;
|
|
6
|
+
readonly type: MimeTypeType;
|
|
7
|
+
readonly subtype: MimeSubtypeType;
|
|
8
|
+
constructor(value: MimeRawType);
|
|
9
|
+
isSatisfiedBy(another: Mime): boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class InvalidMimeError extends Error {
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
export declare class NotAcceptedMimeError extends Error {
|
|
15
|
+
mime: MimeRawType;
|
|
16
|
+
constructor(mime: MimeRawType);
|
|
17
|
+
}
|
|
18
|
+
export {};
|
package/dist/mime.vo.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class Mime {
|
|
2
|
+
constructor(value) {
|
|
3
|
+
const [type, subtype] = value.split("/");
|
|
4
|
+
if (typeof type !== "string" || type.length === 0) {
|
|
5
|
+
throw new InvalidMimeError();
|
|
6
|
+
}
|
|
7
|
+
if (typeof subtype !== "string" || subtype.length === 0) {
|
|
8
|
+
throw new InvalidMimeError();
|
|
9
|
+
}
|
|
10
|
+
this.raw = value;
|
|
11
|
+
this.type = type;
|
|
12
|
+
this.subtype = subtype;
|
|
13
|
+
}
|
|
14
|
+
isSatisfiedBy(another) {
|
|
15
|
+
if (this.raw === another.raw)
|
|
16
|
+
return true;
|
|
17
|
+
const typeMatches = this.type === another.type || this.type === "*";
|
|
18
|
+
if (!typeMatches)
|
|
19
|
+
return false;
|
|
20
|
+
return this.subtype === another.subtype || this.subtype === "*";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class InvalidMimeError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
Object.setPrototypeOf(this, InvalidMimeError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class NotAcceptedMimeError extends Error {
|
|
30
|
+
constructor(mime) {
|
|
31
|
+
super();
|
|
32
|
+
Object.setPrototypeOf(this, NotAcceptedMimeError.prototype);
|
|
33
|
+
this.mime = mime;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
2
|
+
type RateLimiterOptionsType = {
|
|
3
|
+
ms: TimestampType;
|
|
4
|
+
};
|
|
5
|
+
type RateLimiterResultSuccessType = {
|
|
6
|
+
allowed: true;
|
|
7
|
+
};
|
|
8
|
+
type RateLimiterResultErrorType = {
|
|
9
|
+
allowed: false;
|
|
10
|
+
remainingMs: TimestampType;
|
|
11
|
+
};
|
|
12
|
+
type RateLimiterResultType = RateLimiterResultSuccessType | RateLimiterResultErrorType;
|
|
13
|
+
export declare class RateLimiter {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private lastInvocationTimestampMs;
|
|
16
|
+
constructor(options: RateLimiterOptionsType);
|
|
17
|
+
verify(currentTimestampMs: TimestampType): RateLimiterResultType;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class RateLimiter {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.options = options;
|
|
4
|
+
this.lastInvocationTimestampMs = null;
|
|
5
|
+
}
|
|
6
|
+
verify(currentTimestampMs) {
|
|
7
|
+
if (this.lastInvocationTimestampMs === null || this.lastInvocationTimestampMs === undefined) {
|
|
8
|
+
this.lastInvocationTimestampMs = currentTimestampMs;
|
|
9
|
+
return { allowed: true };
|
|
10
|
+
}
|
|
11
|
+
const nextAllowedTimestampMs = this.lastInvocationTimestampMs + this.options.ms;
|
|
12
|
+
if (nextAllowedTimestampMs <= currentTimestampMs) {
|
|
13
|
+
this.lastInvocationTimestampMs = currentTimestampMs;
|
|
14
|
+
return { allowed: true };
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
allowed: false,
|
|
18
|
+
remainingMs: nextAllowedTimestampMs - currentTimestampMs,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
2
|
+
import type { Falsy } from "./ts-utils";
|
|
3
|
+
type RelativeDateType = {
|
|
4
|
+
raw: TimestampType;
|
|
5
|
+
relative: string;
|
|
6
|
+
};
|
|
7
|
+
export declare class RelativeDate {
|
|
8
|
+
static truthy(timestampMs: TimestampType): RelativeDateType;
|
|
9
|
+
static falsy(timestampMs: Falsy<TimestampType>): RelativeDateType | null;
|
|
10
|
+
private static _format;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DateFormatters } from "./date-formatter.service";
|
|
2
|
+
export class RelativeDate {
|
|
3
|
+
static truthy(timestampMs) {
|
|
4
|
+
return RelativeDate._format(timestampMs);
|
|
5
|
+
}
|
|
6
|
+
static falsy(timestampMs) {
|
|
7
|
+
if (!timestampMs)
|
|
8
|
+
return null;
|
|
9
|
+
return RelativeDate._format(timestampMs);
|
|
10
|
+
}
|
|
11
|
+
static _format(timestampMs) {
|
|
12
|
+
return {
|
|
13
|
+
raw: timestampMs,
|
|
14
|
+
relative: DateFormatters.relative(timestampMs),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
export declare enum SizeUnit {
|
|
3
|
+
b = "b",
|
|
4
|
+
kB = "kB",
|
|
5
|
+
MB = "MB",
|
|
6
|
+
GB = "GB"
|
|
7
|
+
}
|
|
8
|
+
declare const SizeValueSchema: z.ZodNumber;
|
|
9
|
+
type SizeValueType = z.infer<typeof SizeValueSchema>;
|
|
10
|
+
type SizeConfigType = {
|
|
11
|
+
unit: SizeUnit;
|
|
12
|
+
value: SizeValueType;
|
|
13
|
+
};
|
|
14
|
+
export declare class Size {
|
|
15
|
+
private readonly unit;
|
|
16
|
+
private readonly value;
|
|
17
|
+
private readonly bytes;
|
|
18
|
+
private static readonly KB_MULTIPLIER;
|
|
19
|
+
private static readonly MB_MULTIPLIER;
|
|
20
|
+
private static readonly GB_MULTIPLIER;
|
|
21
|
+
constructor(config: SizeConfigType);
|
|
22
|
+
toString(): string;
|
|
23
|
+
toBytes(): SizeValueType;
|
|
24
|
+
isGreaterThan(another: Size): boolean;
|
|
25
|
+
format(unit: SizeUnit): string;
|
|
26
|
+
static toBytes(config: SizeConfigType): SizeValueType;
|
|
27
|
+
static unit: typeof SizeUnit;
|
|
28
|
+
private calculateBytes;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
package/dist/size.vo.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { RoundToDecimal } from "./rounding.service";
|
|
3
|
+
export var SizeUnit;
|
|
4
|
+
(function (SizeUnit) {
|
|
5
|
+
SizeUnit["b"] = "b";
|
|
6
|
+
SizeUnit["kB"] = "kB";
|
|
7
|
+
SizeUnit["MB"] = "MB";
|
|
8
|
+
SizeUnit["GB"] = "GB";
|
|
9
|
+
})(SizeUnit || (SizeUnit = {}));
|
|
10
|
+
const SizeValueSchema = z.number().positive();
|
|
11
|
+
export class Size {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.unit = config.unit;
|
|
14
|
+
this.value = SizeValueSchema.parse(config.value);
|
|
15
|
+
this.bytes = this.calculateBytes(config);
|
|
16
|
+
}
|
|
17
|
+
toString() {
|
|
18
|
+
return `${this.value} ${this.unit}`;
|
|
19
|
+
}
|
|
20
|
+
toBytes() {
|
|
21
|
+
return this.bytes;
|
|
22
|
+
}
|
|
23
|
+
isGreaterThan(another) {
|
|
24
|
+
return this.bytes > another.toBytes();
|
|
25
|
+
}
|
|
26
|
+
format(unit) {
|
|
27
|
+
const rounding = new RoundToDecimal(2);
|
|
28
|
+
switch (unit) {
|
|
29
|
+
case SizeUnit.kB: {
|
|
30
|
+
const kbs = rounding.round(this.bytes / Size.KB_MULTIPLIER);
|
|
31
|
+
return `${kbs} ${SizeUnit.kB}`;
|
|
32
|
+
}
|
|
33
|
+
case SizeUnit.MB: {
|
|
34
|
+
const mbs = rounding.round(this.bytes / Size.MB_MULTIPLIER);
|
|
35
|
+
return `${mbs} ${SizeUnit.MB}`;
|
|
36
|
+
}
|
|
37
|
+
case SizeUnit.GB: {
|
|
38
|
+
const gbs = rounding.round(this.bytes / Size.GB_MULTIPLIER);
|
|
39
|
+
return `${gbs} ${SizeUnit.GB}`;
|
|
40
|
+
}
|
|
41
|
+
default: {
|
|
42
|
+
// SizeUnit.b
|
|
43
|
+
return `${this.bytes} ${SizeUnit.b}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
static toBytes(config) {
|
|
48
|
+
return new Size(config).toBytes();
|
|
49
|
+
}
|
|
50
|
+
calculateBytes(config) {
|
|
51
|
+
switch (config.unit) {
|
|
52
|
+
case SizeUnit.kB:
|
|
53
|
+
return config.value * Size.KB_MULTIPLIER;
|
|
54
|
+
case SizeUnit.MB:
|
|
55
|
+
return config.value * Size.MB_MULTIPLIER;
|
|
56
|
+
case SizeUnit.GB:
|
|
57
|
+
return config.value * Size.GB_MULTIPLIER;
|
|
58
|
+
default:
|
|
59
|
+
// SizeUnit.b
|
|
60
|
+
return config.value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
Size.KB_MULTIPLIER = 1024;
|
|
65
|
+
Size.MB_MULTIPLIER = 1024 * Size.KB_MULTIPLIER;
|
|
66
|
+
Size.GB_MULTIPLIER = 1024 * Size.MB_MULTIPLIER;
|
|
67
|
+
Size.unit = SizeUnit;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
2
|
+
type StopwatchResultType = {
|
|
3
|
+
durationMs: TimestampType;
|
|
4
|
+
};
|
|
5
|
+
export declare class Stopwatch {
|
|
6
|
+
private state;
|
|
7
|
+
private readonly startMs;
|
|
8
|
+
private stopMs;
|
|
9
|
+
stop(): StopwatchResultType;
|
|
10
|
+
}
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
var StopwatchState;
|
|
2
|
+
(function (StopwatchState) {
|
|
3
|
+
StopwatchState["started"] = "started";
|
|
4
|
+
StopwatchState["stopped"] = "finished";
|
|
5
|
+
})(StopwatchState || (StopwatchState = {}));
|
|
6
|
+
export class Stopwatch {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.state = StopwatchState.started;
|
|
9
|
+
this.startMs = Date.now();
|
|
10
|
+
}
|
|
11
|
+
stop() {
|
|
12
|
+
if (this.state === StopwatchState.stopped) {
|
|
13
|
+
throw new Error("Stopwatch is already stopped");
|
|
14
|
+
}
|
|
15
|
+
this.state = StopwatchState.stopped;
|
|
16
|
+
this.stopMs = Date.now();
|
|
17
|
+
return { durationMs: this.stopMs - this.startMs };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface TimeResultInterface {
|
|
2
|
+
readonly days: number;
|
|
3
|
+
readonly hours: number;
|
|
4
|
+
readonly minutes: number;
|
|
5
|
+
readonly seconds: number;
|
|
6
|
+
readonly ms: number;
|
|
7
|
+
isAfter(another: TimeResultInterface): boolean;
|
|
8
|
+
isBefore(another: TimeResultInterface): boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class Time {
|
|
11
|
+
static Days(value: number): TimeResultInterface;
|
|
12
|
+
static Hours(value: number): TimeResultInterface;
|
|
13
|
+
static Minutes(value: number): TimeResultInterface;
|
|
14
|
+
static Seconds(value: number): TimeResultInterface;
|
|
15
|
+
static Ms(value: number): TimeResultInterface;
|
|
16
|
+
static Now(now?: number): {
|
|
17
|
+
Minus(time: TimeResultInterface): TimeResultInterface;
|
|
18
|
+
Add(time: TimeResultInterface): TimeResultInterface;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RoundToDecimal } from "./rounding.service";
|
|
2
|
+
const rounding = new RoundToDecimal(2);
|
|
3
|
+
class TimeResult {
|
|
4
|
+
constructor(days, hours, minutes, seconds, ms) {
|
|
5
|
+
this.days = days;
|
|
6
|
+
this.hours = hours;
|
|
7
|
+
this.minutes = minutes;
|
|
8
|
+
this.seconds = seconds;
|
|
9
|
+
this.ms = ms;
|
|
10
|
+
}
|
|
11
|
+
isAfter(another) {
|
|
12
|
+
return this.ms > another.ms;
|
|
13
|
+
}
|
|
14
|
+
isBefore(another) {
|
|
15
|
+
return this.ms < another.ms;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class Time {
|
|
19
|
+
static Days(value) {
|
|
20
|
+
return new TimeResult(value, value * 24, value * 24 * 60, value * 24 * 60 * 60, value * 24 * 60 * 60 * 1000);
|
|
21
|
+
}
|
|
22
|
+
static Hours(value) {
|
|
23
|
+
return new TimeResult(rounding.round(value / 24), value, value * 60, value * 60 * 60, value * 60 * 60 * 1000);
|
|
24
|
+
}
|
|
25
|
+
static Minutes(value) {
|
|
26
|
+
return new TimeResult(rounding.round(value / 60 / 24), rounding.round(value / 60), value, value * 60, value * 60 * 1000);
|
|
27
|
+
}
|
|
28
|
+
static Seconds(value) {
|
|
29
|
+
return new TimeResult(rounding.round(value / 60 / 60 / 24), rounding.round(value / 60 / 60), rounding.round(value / 60), value, value * 1000);
|
|
30
|
+
}
|
|
31
|
+
static Ms(value) {
|
|
32
|
+
return new TimeResult(rounding.round(value / 1000 / 60 / 60 / 24), rounding.round(value / 1000 / 60 / 60), rounding.round(value / 1000 / 60), rounding.round(value / 1000), value);
|
|
33
|
+
}
|
|
34
|
+
static Now(now = Date.now()) {
|
|
35
|
+
return {
|
|
36
|
+
Minus(time) {
|
|
37
|
+
return Time.Ms(now - time.ms);
|
|
38
|
+
},
|
|
39
|
+
Add(time) {
|
|
40
|
+
return Time.Ms(now + time.ms);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/ts-utils.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bgord/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bartosz Gordon",
|
|
@@ -21,12 +21,16 @@
|
|
|
21
21
|
"@biomejs/biome": "1.9.4",
|
|
22
22
|
"@commitlint/cli": "19.8.1",
|
|
23
23
|
"@commitlint/config-conventional": "19.8.1",
|
|
24
|
+
"@types/bun": "1.2.15",
|
|
24
25
|
"cspell": "9.0.2",
|
|
25
|
-
"lefthook": "1.11.
|
|
26
|
+
"lefthook": "1.11.13",
|
|
26
27
|
"only-allow": "1.2.1",
|
|
27
28
|
"shellcheck": "3.1.0",
|
|
28
|
-
"typescript": "5.8.3"
|
|
29
|
-
|
|
29
|
+
"typescript": "5.8.3"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"date-fns": "4.1.0",
|
|
33
|
+
"zod": "3.25.46"
|
|
30
34
|
},
|
|
31
35
|
"sideEffects": false
|
|
32
36
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Time } from "./time.service";
|
|
2
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
3
|
+
|
|
4
|
+
type GetStartOfDayTsInTzConfigType = {
|
|
5
|
+
now: TimestampType;
|
|
6
|
+
timeZoneOffsetMs: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class DateCalculator {
|
|
10
|
+
static getStartOfDayTsInTz(config: GetStartOfDayTsInTzConfigType) {
|
|
11
|
+
const startOfDayUTC = new Date();
|
|
12
|
+
startOfDayUTC.setUTCHours(0, 0, 0, 0);
|
|
13
|
+
|
|
14
|
+
const startOfDayInTimeZone = startOfDayUTC.getTime() + config.timeZoneOffsetMs;
|
|
15
|
+
|
|
16
|
+
const timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay =
|
|
17
|
+
(config.now - startOfDayInTimeZone) % Time.Days(1).ms;
|
|
18
|
+
|
|
19
|
+
if (timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay >= Time.Days(1).ms) {
|
|
20
|
+
return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay + Time.Days(1).ms;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay >= 0) {
|
|
24
|
+
return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return config.now - timeSinceNewDayInTimeZoneRelativeToUtcStartOfDay - Time.Days(1).ms;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { format, formatDistanceToNow } from "date-fns";
|
|
2
|
+
|
|
3
|
+
type FormattedDateType = string;
|
|
4
|
+
|
|
5
|
+
type DateFormattersInputType = Parameters<typeof format>[0];
|
|
6
|
+
|
|
7
|
+
export class DateFormatters {
|
|
8
|
+
static datetime(date: DateFormattersInputType): FormattedDateType {
|
|
9
|
+
return format(date, "yyyy/MM/dd HH:mm");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static date(date: DateFormattersInputType): FormattedDateType {
|
|
13
|
+
return format(date, "yyyy/MM/dd");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static monthDay(date: DateFormattersInputType): FormattedDateType {
|
|
17
|
+
return format(date, "MM/dd");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static relative(date: DateFormattersInputType) {
|
|
21
|
+
return formatDistanceToNow(date, { addSuffix: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/etags.vo.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
const RevisionValueSchema = z.number().int().min(0);
|
|
4
|
+
|
|
5
|
+
type RevisionValueSchemaType = z.infer<typeof RevisionValueSchema>;
|
|
6
|
+
|
|
7
|
+
type ETagValueType = string;
|
|
8
|
+
|
|
9
|
+
export class ETag {
|
|
10
|
+
static HEADER_NAME = "ETag";
|
|
11
|
+
|
|
12
|
+
static IF_MATCH_HEADER_NAME = "if-match";
|
|
13
|
+
|
|
14
|
+
readonly value: ETagValueType;
|
|
15
|
+
|
|
16
|
+
private constructor(readonly revision: RevisionValueSchemaType) {
|
|
17
|
+
this.value = revision.toString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static fromHeader(value?: ETagValueType): ETag | null {
|
|
21
|
+
if (value?.startsWith("W/")) return null;
|
|
22
|
+
|
|
23
|
+
const candidate = Number(value);
|
|
24
|
+
|
|
25
|
+
if (Number.isNaN(candidate)) return null;
|
|
26
|
+
return new ETag(candidate);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type WeakETagValueType = string;
|
|
31
|
+
|
|
32
|
+
export class WeakETag {
|
|
33
|
+
static HEADER_NAME = "ETag";
|
|
34
|
+
|
|
35
|
+
static IF_MATCH_HEADER_NAME = "if-match";
|
|
36
|
+
|
|
37
|
+
readonly value: WeakETagValueType;
|
|
38
|
+
|
|
39
|
+
private constructor(readonly revision: RevisionValueSchemaType) {
|
|
40
|
+
this.value = `W/${revision.toString()}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static fromHeader(value?: WeakETagValueType): WeakETag | null {
|
|
44
|
+
if (!value?.startsWith("W/")) throw Error("Invalid WeakETag");
|
|
45
|
+
|
|
46
|
+
const candidate = Number(value.split("W/")[1]);
|
|
47
|
+
|
|
48
|
+
if (Number.isNaN(candidate)) return null;
|
|
49
|
+
return new WeakETag(candidate);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./date-calculator.service";
|
|
2
|
+
export * from "./date-formatter.service";
|
|
3
|
+
export * from "./dates-of-the-week.vo";
|
|
4
|
+
export * from "./etags.vo";
|
|
5
|
+
export * from "./mime.vo";
|
|
2
6
|
export * from "./noop.service";
|
|
7
|
+
export * from "./rate-limiter.service";
|
|
8
|
+
export * from "./relative-date.vo";
|
|
9
|
+
export * from "./rounding.service";
|
|
10
|
+
export * from "./size.vo";
|
|
11
|
+
export * from "./sleep.service";
|
|
12
|
+
export * from "./stopwatch.service";
|
|
13
|
+
export * from "./time.service";
|
|
14
|
+
export * from "./timestamp.vo";
|
|
15
|
+
export * from "./ts-utils";
|
package/src/mime.vo.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type MimeRawType = string;
|
|
2
|
+
type MimeTypeType = string;
|
|
3
|
+
type MimeSubtypeType = string;
|
|
4
|
+
|
|
5
|
+
export class Mime {
|
|
6
|
+
readonly raw: MimeRawType;
|
|
7
|
+
readonly type: MimeTypeType;
|
|
8
|
+
readonly subtype: MimeSubtypeType;
|
|
9
|
+
|
|
10
|
+
constructor(value: MimeRawType) {
|
|
11
|
+
const [type, subtype] = value.split("/");
|
|
12
|
+
|
|
13
|
+
if (typeof type !== "string" || type.length === 0) {
|
|
14
|
+
throw new InvalidMimeError();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof subtype !== "string" || subtype.length === 0) {
|
|
18
|
+
throw new InvalidMimeError();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.raw = value;
|
|
22
|
+
this.type = type;
|
|
23
|
+
this.subtype = subtype;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isSatisfiedBy(another: Mime): boolean {
|
|
27
|
+
if (this.raw === another.raw) return true;
|
|
28
|
+
|
|
29
|
+
const typeMatches = this.type === another.type || this.type === "*";
|
|
30
|
+
if (!typeMatches) return false;
|
|
31
|
+
|
|
32
|
+
return this.subtype === another.subtype || this.subtype === "*";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class InvalidMimeError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
Object.setPrototypeOf(this, InvalidMimeError.prototype);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class NotAcceptedMimeError extends Error {
|
|
44
|
+
mime: MimeRawType;
|
|
45
|
+
constructor(mime: MimeRawType) {
|
|
46
|
+
super();
|
|
47
|
+
Object.setPrototypeOf(this, NotAcceptedMimeError.prototype);
|
|
48
|
+
this.mime = mime;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
2
|
+
import type { Falsy } from "./ts-utils";
|
|
3
|
+
|
|
4
|
+
type RateLimiterOptionsType = {
|
|
5
|
+
ms: TimestampType;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type RateLimiterResultSuccessType = { allowed: true };
|
|
9
|
+
|
|
10
|
+
type RateLimiterResultErrorType = {
|
|
11
|
+
allowed: false;
|
|
12
|
+
remainingMs: TimestampType;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RateLimiterResultType = RateLimiterResultSuccessType | RateLimiterResultErrorType;
|
|
16
|
+
|
|
17
|
+
export class RateLimiter {
|
|
18
|
+
private lastInvocationTimestampMs: Falsy<TimestampType> = null;
|
|
19
|
+
|
|
20
|
+
constructor(private readonly options: RateLimiterOptionsType) {}
|
|
21
|
+
|
|
22
|
+
verify(currentTimestampMs: TimestampType): RateLimiterResultType {
|
|
23
|
+
if (this.lastInvocationTimestampMs === null || this.lastInvocationTimestampMs === undefined) {
|
|
24
|
+
this.lastInvocationTimestampMs = currentTimestampMs;
|
|
25
|
+
|
|
26
|
+
return { allowed: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const nextAllowedTimestampMs = this.lastInvocationTimestampMs + this.options.ms;
|
|
30
|
+
|
|
31
|
+
if (nextAllowedTimestampMs <= currentTimestampMs) {
|
|
32
|
+
this.lastInvocationTimestampMs = currentTimestampMs;
|
|
33
|
+
|
|
34
|
+
return { allowed: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
allowed: false,
|
|
39
|
+
remainingMs: nextAllowedTimestampMs - currentTimestampMs,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DateFormatters } from "./date-formatter.service";
|
|
2
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
3
|
+
import type { Falsy } from "./ts-utils";
|
|
4
|
+
|
|
5
|
+
type RelativeDateType = { raw: TimestampType; relative: string };
|
|
6
|
+
|
|
7
|
+
export class RelativeDate {
|
|
8
|
+
static truthy(timestampMs: TimestampType): RelativeDateType {
|
|
9
|
+
return RelativeDate._format(timestampMs);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static falsy(timestampMs: Falsy<TimestampType>): RelativeDateType | null {
|
|
13
|
+
if (!timestampMs) return null;
|
|
14
|
+
|
|
15
|
+
return RelativeDate._format(timestampMs);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private static _format(timestampMs: TimestampType): RelativeDateType {
|
|
19
|
+
return {
|
|
20
|
+
raw: timestampMs,
|
|
21
|
+
relative: DateFormatters.relative(timestampMs),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/size.vo.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
import { RoundToDecimal } from "./rounding.service";
|
|
4
|
+
|
|
5
|
+
export enum SizeUnit {
|
|
6
|
+
b = "b",
|
|
7
|
+
kB = "kB",
|
|
8
|
+
MB = "MB",
|
|
9
|
+
GB = "GB",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SizeValueSchema = z.number().positive();
|
|
13
|
+
|
|
14
|
+
export type SizeValueType = z.infer<typeof SizeValueSchema>;
|
|
15
|
+
|
|
16
|
+
type SizeConfigType = { unit: SizeUnit; value: SizeValueType };
|
|
17
|
+
|
|
18
|
+
export class Size {
|
|
19
|
+
private readonly unit: SizeUnit;
|
|
20
|
+
|
|
21
|
+
private readonly value: SizeValueType;
|
|
22
|
+
|
|
23
|
+
private readonly bytes: SizeValueType;
|
|
24
|
+
|
|
25
|
+
private static readonly KB_MULTIPLIER = 1024;
|
|
26
|
+
|
|
27
|
+
private static readonly MB_MULTIPLIER = 1024 * Size.KB_MULTIPLIER;
|
|
28
|
+
|
|
29
|
+
private static readonly GB_MULTIPLIER = 1024 * Size.MB_MULTIPLIER;
|
|
30
|
+
|
|
31
|
+
constructor(config: SizeConfigType) {
|
|
32
|
+
this.unit = config.unit;
|
|
33
|
+
this.value = SizeValueSchema.parse(config.value);
|
|
34
|
+
this.bytes = this.calculateBytes(config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toString(): string {
|
|
38
|
+
return `${this.value} ${this.unit}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toBytes(): SizeValueType {
|
|
42
|
+
return this.bytes;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isGreaterThan(another: Size) {
|
|
46
|
+
return this.bytes > another.toBytes();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
format(unit: SizeUnit): string {
|
|
50
|
+
const rounding = new RoundToDecimal(2);
|
|
51
|
+
|
|
52
|
+
switch (unit) {
|
|
53
|
+
case SizeUnit.kB: {
|
|
54
|
+
const kbs = rounding.round(this.bytes / Size.KB_MULTIPLIER);
|
|
55
|
+
|
|
56
|
+
return `${kbs} ${SizeUnit.kB}`;
|
|
57
|
+
}
|
|
58
|
+
case SizeUnit.MB: {
|
|
59
|
+
const mbs = rounding.round(this.bytes / Size.MB_MULTIPLIER);
|
|
60
|
+
|
|
61
|
+
return `${mbs} ${SizeUnit.MB}`;
|
|
62
|
+
}
|
|
63
|
+
case SizeUnit.GB: {
|
|
64
|
+
const gbs = rounding.round(this.bytes / Size.GB_MULTIPLIER);
|
|
65
|
+
|
|
66
|
+
return `${gbs} ${SizeUnit.GB}`;
|
|
67
|
+
}
|
|
68
|
+
default: {
|
|
69
|
+
// SizeUnit.b
|
|
70
|
+
return `${this.bytes} ${SizeUnit.b}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static toBytes(config: SizeConfigType): SizeValueType {
|
|
76
|
+
return new Size(config).toBytes();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static unit = SizeUnit;
|
|
80
|
+
|
|
81
|
+
private calculateBytes(config: SizeConfigType): SizeValueType {
|
|
82
|
+
switch (config.unit) {
|
|
83
|
+
case SizeUnit.kB:
|
|
84
|
+
return config.value * Size.KB_MULTIPLIER;
|
|
85
|
+
case SizeUnit.MB:
|
|
86
|
+
return config.value * Size.MB_MULTIPLIER;
|
|
87
|
+
case SizeUnit.GB:
|
|
88
|
+
return config.value * Size.GB_MULTIPLIER;
|
|
89
|
+
default:
|
|
90
|
+
// SizeUnit.b
|
|
91
|
+
return config.value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TimestampType } from "./timestamp.vo";
|
|
2
|
+
import type { Falsy } from "./ts-utils";
|
|
3
|
+
|
|
4
|
+
enum StopwatchState {
|
|
5
|
+
started = "started",
|
|
6
|
+
stopped = "finished",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type StopwatchResultType = { durationMs: TimestampType };
|
|
10
|
+
|
|
11
|
+
export class Stopwatch {
|
|
12
|
+
private state: StopwatchState = StopwatchState.started;
|
|
13
|
+
|
|
14
|
+
private readonly startMs: TimestampType = Date.now();
|
|
15
|
+
|
|
16
|
+
private stopMs: Falsy<TimestampType>;
|
|
17
|
+
|
|
18
|
+
stop(): StopwatchResultType {
|
|
19
|
+
if (this.state === StopwatchState.stopped) {
|
|
20
|
+
throw new Error("Stopwatch is already stopped");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.state = StopwatchState.stopped;
|
|
24
|
+
this.stopMs = Date.now();
|
|
25
|
+
|
|
26
|
+
return { durationMs: this.stopMs - this.startMs };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { RoundToDecimal } from "./rounding.service";
|
|
2
|
+
|
|
3
|
+
const rounding = new RoundToDecimal(2);
|
|
4
|
+
|
|
5
|
+
interface TimeResultInterface {
|
|
6
|
+
readonly days: number;
|
|
7
|
+
readonly hours: number;
|
|
8
|
+
readonly minutes: number;
|
|
9
|
+
readonly seconds: number;
|
|
10
|
+
readonly ms: number;
|
|
11
|
+
|
|
12
|
+
isAfter(another: TimeResultInterface): boolean;
|
|
13
|
+
isBefore(another: TimeResultInterface): boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class TimeResult implements TimeResultInterface {
|
|
17
|
+
constructor(
|
|
18
|
+
readonly days: number,
|
|
19
|
+
readonly hours: number,
|
|
20
|
+
readonly minutes: number,
|
|
21
|
+
readonly seconds: number,
|
|
22
|
+
readonly ms: number,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
isAfter(another: TimeResultInterface): boolean {
|
|
26
|
+
return this.ms > another.ms;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isBefore(another: TimeResultInterface): boolean {
|
|
30
|
+
return this.ms < another.ms;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Time {
|
|
35
|
+
static Days(value: number): TimeResultInterface {
|
|
36
|
+
return new TimeResult(
|
|
37
|
+
value,
|
|
38
|
+
value * 24,
|
|
39
|
+
value * 24 * 60,
|
|
40
|
+
value * 24 * 60 * 60,
|
|
41
|
+
value * 24 * 60 * 60 * 1000,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static Hours(value: number): TimeResultInterface {
|
|
46
|
+
return new TimeResult(
|
|
47
|
+
rounding.round(value / 24),
|
|
48
|
+
value,
|
|
49
|
+
value * 60,
|
|
50
|
+
value * 60 * 60,
|
|
51
|
+
value * 60 * 60 * 1000,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static Minutes(value: number): TimeResultInterface {
|
|
56
|
+
return new TimeResult(
|
|
57
|
+
rounding.round(value / 60 / 24),
|
|
58
|
+
rounding.round(value / 60),
|
|
59
|
+
value,
|
|
60
|
+
value * 60,
|
|
61
|
+
value * 60 * 1000,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static Seconds(value: number): TimeResultInterface {
|
|
66
|
+
return new TimeResult(
|
|
67
|
+
rounding.round(value / 60 / 60 / 24),
|
|
68
|
+
rounding.round(value / 60 / 60),
|
|
69
|
+
rounding.round(value / 60),
|
|
70
|
+
value,
|
|
71
|
+
value * 1000,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static Ms(value: number): TimeResultInterface {
|
|
76
|
+
return new TimeResult(
|
|
77
|
+
rounding.round(value / 1000 / 60 / 60 / 24),
|
|
78
|
+
rounding.round(value / 1000 / 60 / 60),
|
|
79
|
+
rounding.round(value / 1000 / 60),
|
|
80
|
+
rounding.round(value / 1000),
|
|
81
|
+
value,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static Now(now = Date.now()) {
|
|
86
|
+
return {
|
|
87
|
+
Minus(time: TimeResultInterface): TimeResultInterface {
|
|
88
|
+
return Time.Ms(now - time.ms);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
Add(time: TimeResultInterface): TimeResultInterface {
|
|
92
|
+
return Time.Ms(now + time.ms);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|