@bccampus/utils 0.1.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 (128) hide show
  1. package/README.md +8 -0
  2. package/dist/array/chunk.d.ts +1 -0
  3. package/dist/array/chunk.js +9 -0
  4. package/dist/array/index.d.ts +1 -0
  5. package/dist/array/index.js +4 -0
  6. package/dist/date/index.d.ts +1 -0
  7. package/dist/date/index.js +8 -0
  8. package/dist/date/iso-date-time.d.ts +127 -0
  9. package/dist/date/iso-date-time.js +256 -0
  10. package/dist/duration/add-durations.d.ts +1 -0
  11. package/dist/duration/add-durations.js +12 -0
  12. package/dist/duration/index.d.ts +3 -0
  13. package/dist/duration/index.js +8 -0
  14. package/dist/duration/is-duration.d.ts +1 -0
  15. package/dist/duration/is-duration.js +7 -0
  16. package/dist/duration/to-number.d.ts +1 -0
  17. package/dist/duration/to-number.js +14 -0
  18. package/dist/format/formatter.d.ts +5 -0
  19. package/dist/format/formatter.js +40 -0
  20. package/dist/format/index.d.ts +3 -0
  21. package/dist/format/index.js +13 -0
  22. package/dist/format/stringify.d.ts +38 -0
  23. package/dist/format/stringify.js +42 -0
  24. package/dist/format/template.d.ts +6 -0
  25. package/dist/format/template.js +35 -0
  26. package/dist/object/get.d.ts +1 -0
  27. package/dist/object/get.js +7 -0
  28. package/dist/object/index.d.ts +5 -0
  29. package/dist/object/index.js +12 -0
  30. package/dist/object/is-object.d.ts +1 -0
  31. package/dist/object/is-object.js +4 -0
  32. package/dist/object/omit.d.ts +1 -0
  33. package/dist/object/omit.js +18 -0
  34. package/dist/object/pick.d.ts +1 -0
  35. package/dist/object/pick.js +11 -0
  36. package/dist/object/set.d.ts +1 -0
  37. package/dist/object/set.js +14 -0
  38. package/dist/set/difference.d.ts +1 -0
  39. package/dist/set/difference.js +10 -0
  40. package/dist/set/index.d.ts +5 -0
  41. package/dist/set/index.js +12 -0
  42. package/dist/set/intersection.d.ts +1 -0
  43. package/dist/set/intersection.js +13 -0
  44. package/dist/set/is-superset.d.ts +1 -0
  45. package/dist/set/is-superset.js +12 -0
  46. package/dist/set/symmetric-difference.d.ts +1 -0
  47. package/dist/set/symmetric-difference.js +14 -0
  48. package/dist/set/union.d.ts +1 -0
  49. package/dist/set/union.js +10 -0
  50. package/dist/string/capitalize.d.ts +1 -0
  51. package/dist/string/capitalize.js +4 -0
  52. package/dist/string/index.d.ts +6 -0
  53. package/dist/string/index.js +14 -0
  54. package/dist/string/pluralize/index.d.ts +2 -0
  55. package/dist/string/pluralize/index.js +4 -0
  56. package/dist/string/pluralize/pluralize.d.ts +85 -0
  57. package/dist/string/pluralize/pluralize.js +210 -0
  58. package/dist/string/pluralize/rules/index.d.ts +4 -0
  59. package/dist/string/pluralize/rules/index.js +10 -0
  60. package/dist/string/pluralize/rules/irregular.d.ts +1 -0
  61. package/dist/string/pluralize/rules/irregular.js +62 -0
  62. package/dist/string/pluralize/rules/pluralization.d.ts +2 -0
  63. package/dist/string/pluralize/rules/pluralization.js +39 -0
  64. package/dist/string/pluralize/rules/singularization.d.ts +2 -0
  65. package/dist/string/pluralize/rules/singularization.js +47 -0
  66. package/dist/string/pluralize/rules/uncountable.d.ts +2 -0
  67. package/dist/string/pluralize/rules/uncountable.js +113 -0
  68. package/dist/string/pluralize/types.d.ts +3 -0
  69. package/dist/string/pluralize/types.js +1 -0
  70. package/dist/string/slugify/index.d.ts +1 -0
  71. package/dist/string/slugify/index.js +2263 -0
  72. package/dist/string/strip-html-tags.d.ts +1 -0
  73. package/dist/string/strip-html-tags.js +7 -0
  74. package/dist/string/to-int.d.ts +9 -0
  75. package/dist/string/to-int.js +10 -0
  76. package/dist/string/to-sentence-case.d.ts +1 -0
  77. package/dist/string/to-sentence-case.js +8 -0
  78. package/dist/string/trim.d.ts +1 -0
  79. package/dist/string/trim.js +11 -0
  80. package/dist/string/truncate.d.ts +1 -0
  81. package/dist/string/truncate.js +9 -0
  82. package/eslint.config.js +19 -0
  83. package/package.json +68 -0
  84. package/src/array/chunk.ts +6 -0
  85. package/src/array/index.ts +1 -0
  86. package/src/date/index.ts +1 -0
  87. package/src/date/iso-date-time.ts +356 -0
  88. package/src/duration/add-durations.ts +16 -0
  89. package/src/duration/index.ts +3 -0
  90. package/src/duration/is-duration.ts +5 -0
  91. package/src/duration/to-number.ts +18 -0
  92. package/src/format/formatter.ts +37 -0
  93. package/src/format/index.ts +3 -0
  94. package/src/format/stringify.ts +87 -0
  95. package/src/format/template.ts +48 -0
  96. package/src/object/get.ts +9 -0
  97. package/src/object/index.ts +5 -0
  98. package/src/object/is-object.ts +2 -0
  99. package/src/object/omit.ts +21 -0
  100. package/src/object/pick.ts +10 -0
  101. package/src/object/set.ts +15 -0
  102. package/src/set/difference.ts +9 -0
  103. package/src/set/index.ts +5 -0
  104. package/src/set/intersection.ts +12 -0
  105. package/src/set/is-superset.ts +12 -0
  106. package/src/set/symmetric-difference.ts +12 -0
  107. package/src/set/union.ts +8 -0
  108. package/src/string/capitalize.ts +2 -0
  109. package/src/string/index.ts +6 -0
  110. package/src/string/pluralize/index.ts +2 -0
  111. package/src/string/pluralize/pluralize.ts +296 -0
  112. package/src/string/pluralize/rules/index.ts +4 -0
  113. package/src/string/pluralize/rules/irregular.ts +59 -0
  114. package/src/string/pluralize/rules/pluralization.ts +40 -0
  115. package/src/string/pluralize/rules/singularization.ts +46 -0
  116. package/src/string/pluralize/rules/uncountable.ts +107 -0
  117. package/src/string/pluralize/types.ts +4 -0
  118. package/src/string/slugify/index.ts +3 -0
  119. package/src/string/strip-html-tags.ts +4 -0
  120. package/src/string/to-int.ts +20 -0
  121. package/src/string/to-sentence-case.ts +6 -0
  122. package/src/string/trim.ts +11 -0
  123. package/src/string/truncate.ts +11 -0
  124. package/tsconfig.json +18 -0
  125. package/tsconfig.lib.json +36 -0
  126. package/tsconfig.lib.tsbuildinfo +1 -0
  127. package/tsconfig.node.json +25 -0
  128. package/vite.config.ts +77 -0
@@ -0,0 +1 @@
1
+ export declare function stripHtmlTags(text: string): string;
@@ -0,0 +1,7 @@
1
+ const htmlTagRegExp = /(<([^>]+)>)/g;
2
+ function stripHtmlTags(text) {
3
+ return text.replace(htmlTagRegExp, "");
4
+ }
5
+ export {
6
+ stripHtmlTags
7
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Converts a string (or a number) into a number.
3
+ * If the input can not be successfully converted, it will return the fallback.
4
+ * It will not return NaN.
5
+ * @param value - The value to convert
6
+ * @param fallback - The value to fallback to. Defaults to 0
7
+ * @returns number
8
+ */
9
+ export declare function stringToInt(value: string | number, fallback?: number): number;
@@ -0,0 +1,10 @@
1
+ function stringToInt(value, fallback = 0) {
2
+ let returnValue = value;
3
+ if (typeof returnValue === "string")
4
+ returnValue = Number.parseInt(returnValue);
5
+ if (Number.isNaN(returnValue)) returnValue = fallback;
6
+ return returnValue;
7
+ }
8
+ export {
9
+ stringToInt
10
+ };
@@ -0,0 +1 @@
1
+ export declare const toSentenceCase: <T extends string>(s: T) => string;
@@ -0,0 +1,8 @@
1
+ const specialCharRegExp = /[\s-_.+]+/g;
2
+ const toSentenceCase = (s) => (s[0].toUpperCase() + s.slice(1).toLowerCase()).replace(
3
+ specialCharRegExp,
4
+ " "
5
+ );
6
+ export {
7
+ toSentenceCase
8
+ };
@@ -0,0 +1 @@
1
+ export declare function trim(text: string, chars?: string | string[]): string;
@@ -0,0 +1,11 @@
1
+ function trim(text, chars = " ") {
2
+ let start = 0;
3
+ let end = text.length;
4
+ if (!Array.isArray(chars)) chars = [chars];
5
+ while (start < end && chars.includes(text[start])) start += 1;
6
+ while (end > start && chars.includes(text[end - 1])) end -= 1;
7
+ return start > 0 || end < text.length ? text.substring(start, end) : text;
8
+ }
9
+ export {
10
+ trim
11
+ };
@@ -0,0 +1 @@
1
+ export declare function truncate(text: string, maxlength?: number, ending?: string): string;
@@ -0,0 +1,9 @@
1
+ function truncate(text, maxlength = 100, ending = "...") {
2
+ if (text.length > maxlength) {
3
+ return text.substring(0, maxlength - ending.length) + ending;
4
+ }
5
+ return text;
6
+ }
7
+ export {
8
+ truncate
9
+ };
@@ -0,0 +1,19 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import tseslint from 'typescript-eslint'
4
+ import { defineConfig, globalIgnores } from 'eslint/config'
5
+
6
+ export default defineConfig([
7
+ globalIgnores(['dist']),
8
+ {
9
+ files: ['**/*.{ts,tsx}'],
10
+ extends: [
11
+ js.configs.recommended,
12
+ tseslint.configs.recommended,
13
+ ],
14
+ languageOptions: {
15
+ ecmaVersion: 2020,
16
+ globals: globals.browser,
17
+ },
18
+ }
19
+ ])
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@bccampus/utils",
3
+ "version": "0.1.0",
4
+ "description": "BCcampus' utility box",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "exports": {
8
+ "./array": {
9
+ "types": "./dist/array/index.d.ts",
10
+ "import": "./dist/array/index.js"
11
+ },
12
+ "./date": {
13
+ "types": "./dist/date/index.d.ts",
14
+ "import": "./dist/date/index.js"
15
+ },
16
+ "./duration": {
17
+ "types": "./dist/duration/index.d.ts",
18
+ "import": "./dist/duration/index.js"
19
+ },
20
+ "./format": {
21
+ "types": "./dist/format/index.d.ts",
22
+ "import": "./dist/format/index.js"
23
+ },
24
+ "./object": {
25
+ "types": "./dist/object/index.d.ts",
26
+ "import": "./dist/object/index.js"
27
+ },
28
+ "./set": {
29
+ "types": "./dist/set/index.d.ts",
30
+ "import": "./dist/set/index.js"
31
+ },
32
+ "./string": {
33
+ "types": "./dist/string/index.d.ts",
34
+ "import": "./dist/string/index.js"
35
+ },
36
+ "./string/pluralize": {
37
+ "types": "./dist/string/pluralize/index.d.ts",
38
+ "import": "./dist/string/pluralize/index.js"
39
+ },
40
+ "./string/slugify": {
41
+ "types": "./dist/string/slugify/index.d.ts",
42
+ "import": "./dist/string/slugify/index.js"
43
+ }
44
+ },
45
+ "packageManager": "yarn@4.15.0",
46
+ "scripts": {
47
+ "check": "tsc -b && eslint ./src",
48
+ "build": "tsc -b && vite build",
49
+ "lint": "eslint .",
50
+ "prepublish": "yarn build",
51
+ "publish": "yarn npm publish --access public --tolerate-republish"
52
+ },
53
+ "devDependencies": {
54
+ "@eslint/js": "^9.39.4",
55
+ "@types/node": "^24.12.3",
56
+ "baseline-browser-mapping": "^2.10.27",
57
+ "eslint": "^9.39.4",
58
+ "globals": "^17.6.0",
59
+ "tinyglobby": "^0.2.16",
60
+ "typescript": "~5.9.3",
61
+ "typescript-eslint": "^8.59.2",
62
+ "vite": "^7.3.3",
63
+ "vite-plugin-dts": "^4.5.4"
64
+ },
65
+ "dependencies": {
66
+ "@sindresorhus/slugify": "^3.0.0"
67
+ }
68
+ }
@@ -0,0 +1,6 @@
1
+ export function chunk<T>(arr: T[], size: number = 5) {
2
+ const chunks: T[][] = [];
3
+ for (let i = 0; i < arr.length; i += size)
4
+ chunks.push(arr.slice(i, i + size));
5
+ return chunks;
6
+ }
@@ -0,0 +1 @@
1
+ export * from "./chunk";
@@ -0,0 +1 @@
1
+ export * from "./iso-date-time";
@@ -0,0 +1,356 @@
1
+ const isoDateTimeRegEx = new RegExp('^([0-9-]+)(T([0-9:.,]+)(Z|[+-]([0-9:]+))?)?$');
2
+ const dateRegEx = new RegExp('^(\\d{4})-?(\\d{2})?-?(\\d{2})?$');
3
+ const timeRegEx = new RegExp('^(\\d{2}):?(\\d{2})?:?(\\d{2})?(\\.(\\d{3}))?$');
4
+ const timezoneRegEx = new RegExp('^Z|^[+-]((\\d{2}):?(\\d{2})?)$');
5
+
6
+ export const InvalidDate: unique symbol = Symbol('Invalid Date');
7
+ export type InvalidDate = typeof InvalidDate;
8
+
9
+ export const DefaultDateFormat: DateFormatOptions = {
10
+ year: 'numeric',
11
+ month: 'long',
12
+ day: '2-digit',
13
+ };
14
+
15
+ export const DefaultTimeFormat: DateFormatOptions = {
16
+ hour: '2-digit',
17
+ minute: '2-digit',
18
+ timeZoneName: 'short',
19
+ };
20
+
21
+ export const DefaultTimezone = 'America/Vancouver';
22
+
23
+ /**
24
+ * Enables storage, retrieval and formating of ISO dates and times
25
+ * with time zone information.
26
+ */
27
+ export class ISODateTime {
28
+ #parsed?: ParsedDateTime;
29
+ #date: Date | InvalidDate;
30
+ #string: string;
31
+
32
+ constructor();
33
+ constructor(isoDatetime: number);
34
+ constructor(isoDatetime: string);
35
+ constructor(isoDatetime?: string | number) {
36
+ if (typeof isoDatetime === 'string') {
37
+ this.#parsed = ISODateTime.parse(isoDatetime);
38
+ if (this.#parsed) {
39
+ this.#date = new Date(ISODateTime.toString(this));
40
+ } else {
41
+ this.#date = InvalidDate;
42
+ console.error(`Failed to parse date for value: ${isoDatetime}`);
43
+ }
44
+ } else if (typeof isoDatetime === 'number' && Number.isFinite(isoDatetime)) {
45
+ this.#date = new Date(isoDatetime);
46
+ this.#parsed = ISODateTime.parse(this.#date.toISOString());
47
+ } else {
48
+ this.#date = new Date();
49
+ this.#parsed = ISODateTime.parse(this.#date.toISOString());
50
+ }
51
+
52
+ if (this.#date !== InvalidDate) this.#string = this.#date.toISOString();
53
+ else this.#string = 'Invalid Date';
54
+ }
55
+
56
+ /**
57
+ * Get date and time components
58
+ */
59
+ get parts() {
60
+ return this.#parsed;
61
+ }
62
+
63
+ /**
64
+ * Get timezone
65
+ */
66
+ get timezone() {
67
+ return this.parts?.timezone ?? 'UTC';
68
+ }
69
+
70
+ /**
71
+ * Parse a string in ISO DateTime format.
72
+ * If the input is valid, returns date and time components;
73
+ * otherwise, returns `undefined`.
74
+ */
75
+ static parse(isoDatetime: string) {
76
+ const isoDatetimeParts = isoDatetime.match(isoDateTimeRegEx);
77
+ if (isoDatetimeParts) {
78
+ const dateParts = isoDatetimeParts[1] && isoDatetimeParts[1].match(dateRegEx);
79
+ if (!dateParts) return undefined;
80
+ const parsedDateTime: ParsedDateTime = {
81
+ date: { year: dateParts[1], month: dateParts[2], day: dateParts[3] },
82
+ };
83
+
84
+ const timeParts = isoDatetimeParts[3] && isoDatetimeParts[3].match(timeRegEx);
85
+ if (timeParts) {
86
+ parsedDateTime.time = {
87
+ hours: timeParts[1],
88
+ minutes: timeParts[2],
89
+ seconds: timeParts[3],
90
+ milliseconds: timeParts[5],
91
+ };
92
+ }
93
+
94
+ const tzParts = isoDatetimeParts[4] && isoDatetimeParts[4].match(timezoneRegEx);
95
+ if (tzParts) {
96
+ parsedDateTime.timezone = tzParts[0] as ISOTimezone;
97
+ }
98
+
99
+ return parsedDateTime;
100
+ }
101
+
102
+ return undefined;
103
+ }
104
+
105
+ static getISOString(parts?: ParsedDateTime) {
106
+ if (!parts) return 'Invalid Date';
107
+ return `${parts.date.year}-${parts.date.month ?? '01'}-${parts.date.day ?? '01'}T${parts.time?.hours ?? '00'}:${parts.time?.minutes ?? '00'}:${parts.time?.seconds ?? '00'}.${parts.time?.milliseconds ?? '000'}${parts.timezone ?? 'Z'}`;
108
+ }
109
+
110
+ /**
111
+ * Retruns ISO date and time string
112
+ */
113
+ static toString(parsedDateTime: string): string;
114
+ static toString(parsedDateTime: ISODateTime): string;
115
+ static toString(dateTime: ISODateTime | string) {
116
+ const _p = typeof dateTime === 'string' ? ISODateTime.parse(dateTime) : dateTime.parts;
117
+
118
+ return ISODateTime.getISOString(_p);
119
+ }
120
+
121
+ /**
122
+ * Returns the stored time value in milliseconds since midnight, January 1, 1970 UTC.
123
+ */
124
+ valueOf() {
125
+ if (this.#date === InvalidDate) return Number.NaN;
126
+
127
+ return this.#date.valueOf();
128
+ }
129
+
130
+ /**
131
+ * Retruns ISO date and time string
132
+ */
133
+ toString() {
134
+ return this.#string;
135
+ }
136
+
137
+ /**
138
+ * Retruns Javascript date object
139
+ */
140
+ toDate() {
141
+ return this.#date;
142
+ }
143
+
144
+ get [Symbol.toStringTag]() {
145
+ return 'ISODateTime';
146
+ }
147
+
148
+ [Symbol.toPrimitive](hint: 'default'): number;
149
+ [Symbol.toPrimitive](hint: 'string'): string;
150
+ [Symbol.toPrimitive](hint: 'number'): number;
151
+ [Symbol.toPrimitive](hint: string) {
152
+ if (hint === 'string') return this.toString();
153
+
154
+ return this.valueOf();
155
+ }
156
+
157
+ toJSON() {
158
+ return this.toString();
159
+ }
160
+
161
+ private static getFormatter(isoDatetime: ISODateTime, config?: DateFormatterConfig) {
162
+ let _format: Partial<Intl.DateTimeFormatOptions> = {};
163
+
164
+ if (config?.customFormat?.dateStyle || config?.customFormat?.timeStyle) {
165
+ _format = {
166
+ dateStyle: config?.customFormat?.dateStyle,
167
+ timeStyle: config?.customFormat?.timeStyle,
168
+ };
169
+ } else {
170
+ if (config?.noDate === true && config?.noTime === true) {
171
+ _format = { ...config.customFormat };
172
+ } else if (config?.noDate === true && !config?.noTime) {
173
+ _format = { ...DefaultTimeFormat, ...config.customFormat };
174
+ } else if (config?.noTime === true && !config.noDate) {
175
+ _format = { ...DefaultDateFormat, ...config.customFormat };
176
+ } else {
177
+ _format = { ...DefaultDateFormat, ...DefaultTimeFormat, ...config?.customFormat };
178
+ }
179
+ }
180
+
181
+ if (config?.customFormat?.timeZone) _format.timeZone = config.customFormat.timeZone;
182
+ else if (isoDatetime.parts?.time === undefined) _format.timeZone = 'UTC';
183
+ else _format.timeZone = DefaultTimezone;
184
+
185
+ return new Intl.DateTimeFormat('en', _format);
186
+ }
187
+
188
+ /**
189
+ * Returns formated date and time string
190
+ */
191
+ static format(isoDatetime: string, config?: DateFormatterConfig): string;
192
+ static format(isoDatetime: ISODateTime, config?: DateFormatterConfig): string;
193
+ static format(isoDatetime: ISODateTime | string, config?: DateFormatterConfig) {
194
+ const _isoDatetime = typeof isoDatetime === 'string' ? new ISODateTime(isoDatetime) : isoDatetime;
195
+
196
+ const _date = _isoDatetime.toDate();
197
+ if (_date === InvalidDate) return _isoDatetime.toString();
198
+
199
+ const formatter = ISODateTime.getFormatter(_isoDatetime, config);
200
+ return formatter.format(_date);
201
+ }
202
+
203
+ /**
204
+ * Returns formated date and time range string
205
+ */
206
+ static formatRange(startDate: string, endDate: string, config?: DateFormatterConfig): string;
207
+ static formatRange(startDate: ISODateTime, endDate: ISODateTime, config?: DateFormatterConfig): string;
208
+ static formatRange(startDate: ISODateTime | string, endDate: ISODateTime | string, config?: DateFormatterConfig) {
209
+ const _startISODateTime = typeof startDate === 'string' ? new ISODateTime(startDate) : startDate;
210
+
211
+ const _startDate = _startISODateTime.toDate();
212
+ if (_startDate === InvalidDate) return _startISODateTime.toString();
213
+
214
+ const _endISODateTime = typeof endDate === 'string' ? new ISODateTime(endDate) : endDate;
215
+ const _endDate = _endISODateTime.toDate();
216
+ if (_endDate === InvalidDate) return _endISODateTime.toString();
217
+
218
+ if (_startISODateTime.timezone !== _endISODateTime.timezone) throw 'Start and end dates in different time zones';
219
+
220
+ const formatter = ISODateTime.getFormatter(_startISODateTime, config);
221
+
222
+ const formatedRange = formatter.formatRange(_startDate, _endDate).replace(/\s/g, ' ');
223
+
224
+ // normalize space usage,
225
+ // `formatRange` uses u+202f to separate the parts in some systems and the standard space in the others
226
+ return formatedRange.replace(/\s/g, ' ');
227
+ }
228
+
229
+ /**
230
+ * Returns formated date and time string
231
+ */
232
+ format(config?: DateFormatterConfig) {
233
+ return ISODateTime.format(this, config);
234
+ }
235
+
236
+ /**
237
+ * Adds milliseconds to the date and returns it as a new ISODateTime object
238
+ */
239
+ addMillisecond(milliseconds: number) {
240
+ return new ISODateTime(this.valueOf() + milliseconds);
241
+ }
242
+ /**
243
+ * Adds seconds to the date and returns it as a new ISODateTime object
244
+ */
245
+ addSecond(seconds: number) {
246
+ return this.addMillisecond(seconds * 1000);
247
+ }
248
+ /**
249
+ * Adds minutes to the date and returns it as a new ISODateTime object
250
+ */
251
+ addMinute(minutes: number) {
252
+ return this.addMillisecond(minutes * 60_000);
253
+ }
254
+ /**
255
+ * Adds hours to the date and returns it as a new ISODateTime object
256
+ */
257
+ addHour(hours: number) {
258
+ return this.addMillisecond(hours * 3_600_000);
259
+ }
260
+ /**
261
+ * Adds days to the date and returns it as a new ISODateTime object
262
+ */
263
+ addDay(days: number) {
264
+ return this.addMillisecond(days * 86_400_000);
265
+ }
266
+
267
+ /**
268
+ * Returns the start of the previous month as a new ISODateTime object
269
+ */
270
+ static getPrevMonthStart(date: ISODateTime) {
271
+ if (date.parts) {
272
+ const month = date.parts.date.month ? Number(date.parts.date.month) : 1;
273
+ const prevMonth = month === 1 ? 12 : month - 1;
274
+ const year = month === 1 ? (Number(date.parts.date.year) - 1).toString() : date.parts.date.year;
275
+ const prevParts: ParsedDateTime = {
276
+ date: {
277
+ year: year,
278
+ month: prevMonth.toString().padStart(2, '0'),
279
+ day: '01',
280
+ },
281
+ timezone: date.parts.timezone,
282
+ };
283
+
284
+ return new ISODateTime(ISODateTime.getISOString(prevParts));
285
+ }
286
+
287
+ return date;
288
+ }
289
+
290
+ /**
291
+ * Returns the start of the previous month as a new ISODateTime object
292
+ */
293
+ getPrevMonthStart() {
294
+ return ISODateTime.getPrevMonthStart(this);
295
+ }
296
+
297
+ /**
298
+ * Returns the start of the next month as a new ISODateTime object
299
+ */
300
+ static getNextMonthStart(date: ISODateTime) {
301
+ if (date.parts) {
302
+ const month = date.parts.date.month ? Number(date.parts.date.month) : 1;
303
+ const nextMonth = (month % 12) + 1;
304
+ const year = month === 12 ? (Number(date.parts.date.year) + 1).toString() : date.parts.date.year;
305
+
306
+ const nextParts: ParsedDateTime = {
307
+ date: {
308
+ year: year,
309
+ month: nextMonth.toString().padStart(2, '0'),
310
+ day: '01',
311
+ },
312
+ timezone: date.parts.timezone,
313
+ };
314
+
315
+ return new ISODateTime(ISODateTime.getISOString(nextParts));
316
+ }
317
+
318
+ return date;
319
+ }
320
+
321
+ /**
322
+ * Returns the start of the next month as a new ISODateTime object
323
+ */
324
+ getNextMonthStart() {
325
+ return ISODateTime.getNextMonthStart(this);
326
+ }
327
+ }
328
+
329
+ interface ISODateParts {
330
+ year: string;
331
+ month?: string;
332
+ day?: string;
333
+ }
334
+
335
+ interface ISOTimeParts {
336
+ hours: string;
337
+ minutes?: string;
338
+ seconds?: string;
339
+ milliseconds?: string;
340
+ }
341
+
342
+ type ISOTimezone = 'Z' | `${'+' | '-'}${string}`;
343
+
344
+ export interface ParsedDateTime {
345
+ date: ISODateParts;
346
+ time?: ISOTimeParts;
347
+ timezone?: ISOTimezone;
348
+ }
349
+
350
+ export type DateFormatOptions = Partial<Intl.DateTimeFormatOptions>;
351
+
352
+ export interface DateFormatterConfig {
353
+ noDate?: boolean;
354
+ noTime?: boolean;
355
+ customFormat?: DateFormatOptions;
356
+ }
@@ -0,0 +1,16 @@
1
+ import { toNumber } from "./to-number";
2
+
3
+ export function addDuration(left: string, right: string) {
4
+ const totalDurationInSeconds =
5
+ toNumber(left, "second") + toNumber(right, "second");
6
+
7
+ const hours = Math.floor(totalDurationInSeconds / 3600)
8
+ .toString()
9
+ .padStart(2, "0");
10
+ const minutes = Math.floor((totalDurationInSeconds % 3600) / 60)
11
+ .toString()
12
+ .padStart(2, "0");
13
+ const seconds = (totalDurationInSeconds % 60).toString().padStart(2, "0");
14
+
15
+ return `${hours}:${minutes}:${seconds}`;
16
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./is-duration";
2
+ export * from "./to-number";
3
+ export * from "./add-durations";
@@ -0,0 +1,5 @@
1
+ const durationRegExp = /^\d{2}:\d{2}:\d{2}$/;
2
+
3
+ export function isDuration(duration: string) {
4
+ return durationRegExp.test(duration);
5
+ }
@@ -0,0 +1,18 @@
1
+ import { isDuration } from "./is-duration";
2
+
3
+ export function toNumber(
4
+ duration: string,
5
+ unit: "hour" | "minute" | "second" = "hour",
6
+ ) {
7
+ if (!isDuration(duration)) {
8
+ console.error(new Error(`Malformed duration value: ${duration}`));
9
+ return 0;
10
+ }
11
+
12
+ const parts = duration.split(":").map((p) => Number(p));
13
+
14
+ if (unit === "minute") return parts[0] * 60 + parts[1] + parts[2] / 60;
15
+ if (unit === "second") return parts[0] * 3600 + parts[1] * 60 + parts[2];
16
+
17
+ return parts[0] + parts[1] / 60 + parts[2] / 3600;
18
+ }
@@ -0,0 +1,37 @@
1
+ export const dateFormatter = new Intl.DateTimeFormat("en", {
2
+ dateStyle: "full",
3
+ timeStyle: "short",
4
+ timeZone: "America/Vancouver",
5
+ });
6
+ export const formatDate = (date?: Date | null) => {
7
+ if (date) return dateFormatter.format(date);
8
+ return "";
9
+ };
10
+
11
+ const listFormatter = new Intl.ListFormat("en", {
12
+ style: "long",
13
+ type: "conjunction",
14
+ });
15
+
16
+ export const formatList = (list?: string[] | null) => {
17
+ if (Array.isArray(list)) return listFormatter.format(list);
18
+ return "";
19
+ };
20
+
21
+ const numberFormatter = new Intl.NumberFormat("en", {
22
+ maximumFractionDigits: 2,
23
+ });
24
+ export const formatNumber = (num?: number | null) => {
25
+ if (num) return numberFormatter.format(num);
26
+ return "";
27
+ };
28
+
29
+ const currencyFormatter = new Intl.NumberFormat("en", {
30
+ style: "currency",
31
+ currency: "CAD",
32
+ maximumFractionDigits: 2,
33
+ });
34
+ export const formatCurrency = (num?: number | null) => {
35
+ if (num) return currencyFormatter.format(num);
36
+ return "";
37
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./formatter";
2
+ export * from "./stringify";
3
+ export * from "./template";