iamcal 1.1.1 → 2.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 (54) hide show
  1. package/README.md +47 -11
  2. package/lib/component.d.ts +62 -0
  3. package/lib/component.d.ts.map +1 -0
  4. package/lib/component.js +208 -0
  5. package/lib/components/Calendar.d.ts +66 -0
  6. package/lib/components/Calendar.d.ts.map +1 -0
  7. package/lib/components/Calendar.js +121 -0
  8. package/lib/components/CalendarEvent.d.ts +109 -0
  9. package/lib/components/CalendarEvent.d.ts.map +1 -0
  10. package/lib/components/CalendarEvent.js +223 -0
  11. package/lib/components/TimeZone.d.ts +74 -0
  12. package/lib/components/TimeZone.d.ts.map +1 -0
  13. package/lib/components/TimeZone.js +127 -0
  14. package/lib/components/TimeZoneOffset.d.ts +103 -0
  15. package/lib/components/TimeZoneOffset.d.ts.map +1 -0
  16. package/lib/components/TimeZoneOffset.js +148 -0
  17. package/lib/components/index.d.ts +5 -0
  18. package/lib/components/index.d.ts.map +1 -0
  19. package/lib/components/index.js +21 -0
  20. package/lib/date.d.ts +165 -0
  21. package/lib/date.d.ts.map +1 -0
  22. package/lib/date.js +373 -0
  23. package/lib/index.d.ts +6 -153
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +21 -469
  26. package/lib/io.d.ts +10 -8
  27. package/lib/io.d.ts.map +1 -1
  28. package/lib/io.js +18 -13
  29. package/lib/parse.d.ts +33 -16
  30. package/lib/parse.d.ts.map +1 -1
  31. package/lib/parse.js +59 -56
  32. package/lib/patterns.d.ts +36 -0
  33. package/lib/patterns.d.ts.map +1 -0
  34. package/lib/patterns.js +50 -0
  35. package/lib/property.d.ts +149 -0
  36. package/lib/property.d.ts.map +1 -0
  37. package/lib/property.js +450 -0
  38. package/package.json +48 -43
  39. package/src/component.ts +248 -0
  40. package/src/components/Calendar.ts +162 -0
  41. package/src/components/CalendarEvent.ts +270 -0
  42. package/src/components/TimeZone.ts +152 -0
  43. package/src/components/TimeZoneOffset.ts +187 -0
  44. package/src/components/index.ts +4 -0
  45. package/src/date.ts +395 -0
  46. package/src/index.ts +6 -576
  47. package/src/io.ts +17 -13
  48. package/src/parse.ts +72 -52
  49. package/src/patterns.ts +69 -0
  50. package/src/property.ts +492 -0
  51. package/lib/types.d.ts +0 -6
  52. package/lib/types.d.ts.map +0 -1
  53. package/lib/types.js +0 -3
  54. package/src/types.ts +0 -5
package/src/date.ts ADDED
@@ -0,0 +1,395 @@
1
+ import * as patterns from './patterns'
2
+ import { getPropertyValueType, type Property } from './property'
3
+
4
+ export const ONE_SECOND_MS = 1000
5
+ export const ONE_MINUTE_MS = 60 * ONE_SECOND_MS
6
+ export const ONE_HOUR_MS = 60 * ONE_MINUTE_MS
7
+ export const ONE_DAY_MS = 24 * ONE_HOUR_MS
8
+
9
+ export interface CalendarDateOrTime {
10
+ /**
11
+ * Create a property from this date.
12
+ * @param name The name of the property.
13
+ */
14
+ toProperty(name: string): Property
15
+
16
+ /**
17
+ * Get the string value of this date.
18
+ * @returns An iCalendar date or date-time string.
19
+ */
20
+ getValue(): string
21
+
22
+ /**
23
+ * Get the date value of this date. For {@link CalendarDate} this is the
24
+ * time at the start of the day.
25
+ * @returns The date value of this date.
26
+ */
27
+ getDate(): Date
28
+
29
+ /**
30
+ * Check if this date represents a full day, as opposed to a date-time.
31
+ * @returns `true` if this object is a {@link CalendarDate}.
32
+ */
33
+ isFullDay(): boolean
34
+ }
35
+
36
+ /**
37
+ * Represents a DATE value as defined by RFC5545.
38
+ * This is a date without a time, representing a whole day.
39
+ */
40
+ export class CalendarDate implements CalendarDateOrTime {
41
+ private date: Date
42
+
43
+ constructor(date: Date | string | CalendarDateOrTime) {
44
+ if (typeof date === 'object') {
45
+ if (Object.prototype.toString.call(date) === '[object Date]') {
46
+ this.date = date as Date
47
+ } else {
48
+ this.date = (date as CalendarDateOrTime).getDate()
49
+ }
50
+ } else {
51
+ try {
52
+ this.date = parseDateString(date)
53
+ } catch {
54
+ this.date = new Date(date)
55
+ }
56
+ }
57
+
58
+ if (!this.date || isNaN(this.date.getTime())) {
59
+ throw new Error('Invalid date provided')
60
+ }
61
+
62
+ this.date.setHours(0, 0, 0, 0)
63
+ }
64
+
65
+ toProperty(name: string): Property {
66
+ return {
67
+ name,
68
+ params: ['VALUE=DATE'],
69
+ value: this.getValue(),
70
+ }
71
+ }
72
+
73
+ getValue(): string {
74
+ return toDateString(this.date)
75
+ }
76
+
77
+ getDate(): Date {
78
+ return new Date(this.date)
79
+ }
80
+
81
+ isFullDay(): boolean {
82
+ return true
83
+ }
84
+ }
85
+
86
+ export class CalendarDateTime implements CalendarDateOrTime {
87
+ private date: Date
88
+
89
+ constructor(date: Date | string | CalendarDateOrTime) {
90
+ if (typeof date === 'object') {
91
+ if (Object.prototype.toString.call(date) === '[object Date]') {
92
+ this.date = date as Date
93
+ } else {
94
+ this.date = (date as CalendarDateOrTime).getDate()
95
+ }
96
+ } else {
97
+ try {
98
+ this.date = parseDateTimeString(date)
99
+ } catch {
100
+ this.date = new Date(date)
101
+ }
102
+ }
103
+
104
+ if (!this.date || isNaN(this.date.getTime())) {
105
+ throw new Error('Invalid date provided')
106
+ }
107
+ }
108
+
109
+ toProperty(name: string): Property {
110
+ return {
111
+ name,
112
+ params: [],
113
+ value: this.getValue(),
114
+ }
115
+ }
116
+
117
+ getValue(): string {
118
+ return toDateTimeString(this.date)
119
+ }
120
+
121
+ getDate(): Date {
122
+ return new Date(this.date)
123
+ }
124
+
125
+ isFullDay(): boolean {
126
+ return false
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Pad a number with leading zeros.
132
+ * @param num The number to pad with zeros.
133
+ * @param length How many digits the resulting string should have.
134
+ * @returns The padded string.
135
+ * @throws If the digits of `num` is greater than `length`.
136
+ * @throws If `num` is NaN, a decimal or negative.
137
+ * @throws If `length` is not NaN, a decimal or less than 1.
138
+ */
139
+ export function padZeros(num: number, length: number): string {
140
+ if (isNaN(num)) throw new Error('Number must not be NaN')
141
+ if (isNaN(length)) throw new Error('Length must not be NaN')
142
+ if (num < 0) throw new Error('Number must not be negative')
143
+ if (length <= 0) throw new Error('Length must not be less than 1')
144
+ if (num % 1 !== 0) throw new Error('Number must be an integer')
145
+ if (length % 1 !== 0) throw new Error('Length must be an integer')
146
+
147
+ const digits = Math.floor(Math.log10(num)) + 1
148
+ if (num !== 0 && digits > length)
149
+ throw new Error('Number must not have more digits than length')
150
+
151
+ return String(num).padStart(length, '0')
152
+ }
153
+
154
+ /**
155
+ * Pad zeros for the year component of a date string.
156
+ * @param year The year, should be positive.
157
+ * @returns The 4-digit padded year.
158
+ */
159
+ export function padYear(year: number): string {
160
+ return padZeros(year, 4)
161
+ }
162
+
163
+ /**
164
+ * Pad zeros for the month component of a date string.
165
+ * @param month The month, should be between 1 and 12.
166
+ * @returns The 2-digit padded month.
167
+ */
168
+ export function padMonth(month: number): string {
169
+ return padZeros(month, 2)
170
+ }
171
+
172
+ /**
173
+ * Pad zeros for the day component of a date string.
174
+ * @param day The day, should be between 1 and 31.
175
+ * @returns The 2-digit padded day.
176
+ */
177
+ export function padDay(day: number): string {
178
+ return padZeros(day, 2)
179
+ }
180
+
181
+ /**
182
+ * Pad zeros for the hours component of a time string.
183
+ * @param hours The hours, should be between 0 and 23.
184
+ * @returns The 2-digit padded hours.
185
+ */
186
+ export function padHours(hours: number): string {
187
+ return padZeros(hours, 2)
188
+ }
189
+
190
+ /**
191
+ * Pad zeros for the minutes component of a time string.
192
+ * @param minutes The minutes, should be between 0 and 59.
193
+ * @returns The 2-digit padded minutes.
194
+ */
195
+ export function padMinutes(minutes: number): string {
196
+ return padZeros(minutes, 2)
197
+ }
198
+
199
+ /**
200
+ * Pad zeros for the seconds component of a time string.
201
+ * @param seconds The seconds, should be between 0 and 59.
202
+ * @returns The 2-digit padded seconds.
203
+ */
204
+ export function padSeconds(seconds: number): string {
205
+ return padZeros(seconds, 2)
206
+ }
207
+
208
+ /**
209
+ * Parse a date property into a {@link CalendarDateOrTime}.
210
+ * @param dateProperty The property to parse.
211
+ * @param defaultType The default value type to be used if the property does not have an explicit value type.
212
+ * @returns The parsed date as a {@link CalendarDateOrTime}.
213
+ * @throws If the value is invalid for the value type.
214
+ * @throws If the value type is not `DATE-TIME` or `DATE`.
215
+ */
216
+ export function parseDateProperty(
217
+ dateProperty: Property,
218
+ defaultType: 'DATE-TIME' | 'DATE' = 'DATE-TIME'
219
+ ): CalendarDateOrTime {
220
+ const value = dateProperty.value
221
+ const valueType = getPropertyValueType(dateProperty, defaultType)
222
+
223
+ if (valueType === 'DATE-TIME') {
224
+ return new CalendarDateTime(value)
225
+ } else if (valueType === 'DATE') {
226
+ return new CalendarDate(value)
227
+ } else {
228
+ throw new Error(`Illegal value type for date '${valueType}'`)
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Format a date as an iCalendar compatible time string. The day of the date is
234
+ * ignored.
235
+ * @param date The date to convert to a time string.
236
+ * @returns The time of the date formatted according to the iCalendar specification.
237
+ * @throws If the date is invalid.
238
+ */
239
+ export function toTimeString(date: Date): string {
240
+ if (isNaN(date.getTime())) throw new Error('Date is invalid')
241
+ return `${padHours(date.getHours())}${padMinutes(date.getMinutes())}${padSeconds(date.getSeconds())}`
242
+ }
243
+
244
+ /**
245
+ * Format a date as an iCalendar compatible date string.
246
+ * @param date The date to convert to a date string.
247
+ * @returns A date string formatted according to the iCalendar specification.
248
+ * @throws If the date is invalid.
249
+ */
250
+ export function toDateString(date: Date): string {
251
+ if (isNaN(date.getTime())) throw new Error('Date is invalid')
252
+ return `${padYear(date.getFullYear())}${padMonth(date.getMonth() + 1)}${padDay(date.getDate())}`
253
+ }
254
+
255
+ /**
256
+ * Format a date as an iCalendar compatible date-time string.
257
+ * @param date The date to convert to a date-time string.
258
+ * @returns A date-time string formatted according to the iCalendar specification.
259
+ * @throws If the date is invalid.
260
+ */
261
+ export function toDateTimeString(date: Date): string {
262
+ if (isNaN(date.getTime())) throw new Error('Date is invalid')
263
+ return `${toDateString(date)}T${toTimeString(date)}`
264
+ }
265
+
266
+ /**
267
+ * Format a date as an iCalendar compatible date-time string. Offsets the date
268
+ * to UTC using `timeZoneOffset`.
269
+ * @param date The date in local time to convert to UTC and a date-time string.
270
+ * @param timeZoneOffset The timezone offset in minutes.
271
+ * @returns A UTC date-time string formatted according to the iCalendar specification.
272
+ * @throws If the date is invalid.
273
+ * @throws If the offset is invalid.
274
+ * @example
275
+ * // The timezone offset for CET (Central European Time)
276
+ * const timeZoneOffsetCET = -60 // +01:00
277
+ * const date = new Date('2025-08-07T12:00:00') // The time in CET
278
+ * // Returns "20250807T110000Z"
279
+ * const utcDate = toDateTimeStringUTC(date, timeZoneOffsetCET)
280
+ */
281
+ export function toDateTimeStringUTC(
282
+ date: Date,
283
+ timeZoneOffset: number
284
+ ): string {
285
+ if (isNaN(date.getTime())) throw new Error('Date is invalid')
286
+ if (isNaN(timeZoneOffset)) throw new Error('Time zone offset cannot be NaN')
287
+ const utcDate = new Date(date.getTime() + timeZoneOffset * ONE_MINUTE_MS)
288
+ return `${toDateString(utcDate)}T${toTimeString(utcDate)}Z`
289
+ }
290
+
291
+ /**
292
+ * Parse a date-time string to a `Date`.
293
+ * @param dateTime A date-time string formatted according to the iCalendar specification.
294
+ * @returns The parsed date-time.
295
+ * @throws If the date is invalid.
296
+ */
297
+ export function parseDateTimeString(dateTime: string): Date {
298
+ if (!patterns.valueTypeDateTime.test(dateTime)) {
299
+ throw new Error('Date-time has invalid format')
300
+ }
301
+
302
+ const year = parseInt(dateTime.substring(0, 4))
303
+ const monthIndex = parseInt(dateTime.substring(4, 6)) - 1
304
+ const day = parseInt(dateTime.substring(6, 8))
305
+
306
+ if (monthIndex < 0 || monthIndex > 11) {
307
+ throw new Error('Date-time is invalid')
308
+ }
309
+ const daysInMonth = new Date(year, monthIndex + 1, 0).getDate()
310
+ if (day < 1 || day > daysInMonth) {
311
+ throw new Error('Date-time is invalid')
312
+ }
313
+
314
+ const hours = parseInt(dateTime.substring(9, 11))
315
+ if (hours < 0 || hours > 23) {
316
+ throw new Error('Date-time is invalid')
317
+ }
318
+ const minutes = parseInt(dateTime.substring(11, 13))
319
+ if (minutes < 0 || minutes > 59) {
320
+ throw new Error('Date-time is invalid')
321
+ }
322
+ const seconds = parseInt(dateTime.substring(13, 15))
323
+ if (seconds < 0 || seconds > 59) {
324
+ throw new Error('Date-time is invalid')
325
+ }
326
+
327
+ if (dateTime.endsWith('Z')) {
328
+ const time = Date.UTC(year, monthIndex, day, hours, minutes, seconds)
329
+ return new Date(time)
330
+ } else {
331
+ const parsedDate = new Date(
332
+ year,
333
+ monthIndex,
334
+ day,
335
+ hours,
336
+ minutes,
337
+ seconds
338
+ )
339
+ return parsedDate
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Parse a date string to a `Date`.
345
+ * @param date A date-time string formatted according to the iCalendar specification.
346
+ * @returns The parsed date.
347
+ * @throws If the date is invalid.
348
+ */
349
+ export function parseDateString(date: string): Date {
350
+ if (!patterns.matchesWholeString(patterns.valueTypeDate, date)) {
351
+ throw new Error('Date has invalid format')
352
+ }
353
+
354
+ const year = parseInt(date.substring(0, 4))
355
+ const monthIndex = parseInt(date.substring(4, 6)) - 1
356
+ const day = parseInt(date.substring(6, 8))
357
+
358
+ if (monthIndex < 0 || monthIndex > 11) {
359
+ throw new Error('Date is invalid')
360
+ }
361
+ const daysInMonth = new Date(year, monthIndex + 1, 0).getDate()
362
+ if (day < 1 || day > daysInMonth) {
363
+ throw new Error('Date is invalid')
364
+ }
365
+
366
+ const parsedDate = new Date(year, monthIndex, day)
367
+ return parsedDate
368
+ }
369
+
370
+ /**
371
+ * Convert `Date` objects to a {@link CalendarDateOrTime} object or return as
372
+ * is if `date` is already a {@link CalendarDateOrTime}.
373
+ * @param date The date object to convert.
374
+ * @param fullDay If `true`, a `Date` object is converted to {@link CalendarDate}, otherwise `Date` is converted to {@link CalendarDateTime}.
375
+ * @returns A {@link CalendarDateOrTime} as described above.
376
+ */
377
+ export function convertDate<T extends CalendarDateOrTime>(
378
+ date: Date | T,
379
+ fullDay?: false
380
+ ): T | CalendarDateTime
381
+ export function convertDate<T extends CalendarDateOrTime>(
382
+ date: Date | T,
383
+ fullDay: true
384
+ ): T | CalendarDate
385
+ export function convertDate(
386
+ date: Date | CalendarDateOrTime,
387
+ fullDay: boolean = false
388
+ ): CalendarDateOrTime {
389
+ if (Object.prototype.toString.call(date) === '[object Date]') {
390
+ if (fullDay) return new CalendarDate(date as Date)
391
+ else return new CalendarDateTime(date as Date)
392
+ } else {
393
+ return date as CalendarDateOrTime
394
+ }
395
+ }