iamcal 2.0.0 → 2.1.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/README.md +47 -11
- package/lib/component.d.ts +46 -9
- package/lib/component.d.ts.map +1 -1
- package/lib/component.js +105 -17
- package/lib/components/Calendar.d.ts +35 -14
- package/lib/components/Calendar.d.ts.map +1 -1
- package/lib/components/Calendar.js +43 -15
- package/lib/components/CalendarEvent.d.ts +84 -22
- package/lib/components/CalendarEvent.d.ts.map +1 -1
- package/lib/components/CalendarEvent.js +142 -67
- package/lib/components/TimeZone.d.ts +62 -39
- package/lib/components/TimeZone.d.ts.map +1 -1
- package/lib/components/TimeZone.js +81 -86
- package/lib/components/TimeZoneOffset.d.ts +103 -0
- package/lib/components/TimeZoneOffset.d.ts.map +1 -0
- package/lib/components/TimeZoneOffset.js +148 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/components/index.js +2 -1
- package/lib/date.d.ts +165 -0
- package/lib/date.d.ts.map +1 -0
- package/lib/date.js +373 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -2
- package/lib/io.d.ts +9 -7
- package/lib/io.d.ts.map +1 -1
- package/lib/io.js +16 -11
- package/lib/parse.d.ts +32 -15
- package/lib/parse.d.ts.map +1 -1
- package/lib/parse.js +55 -53
- package/lib/patterns.d.ts +36 -0
- package/lib/patterns.d.ts.map +1 -0
- package/lib/patterns.js +50 -0
- package/lib/property.d.ts +149 -0
- package/lib/property.d.ts.map +1 -0
- package/lib/property.js +450 -0
- package/package.json +50 -38
- package/src/component.ts +132 -23
- package/src/components/Calendar.ts +58 -20
- package/src/components/CalendarEvent.ts +170 -66
- package/src/components/TimeZone.ts +86 -96
- package/src/components/TimeZoneOffset.ts +187 -0
- package/src/components/index.ts +2 -1
- package/src/date.ts +395 -0
- package/src/index.ts +3 -1
- package/src/io.ts +16 -11
- package/src/parse.ts +71 -51
- package/src/patterns.ts +69 -0
- package/src/property.ts +492 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Component, ComponentValidationError } from '../component'
|
|
2
|
+
import { CalendarDateOrTime, convertDate, parseDateProperty } from '../date'
|
|
3
|
+
import { AllowedPropertyName } from '../property'
|
|
4
|
+
|
|
5
|
+
export const knownOffsetTypes = ['DAYLIGHT', 'STANDARD']
|
|
6
|
+
export type OffsetType = (typeof knownOffsetTypes)[number]
|
|
7
|
+
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
|
|
8
|
+
export type Offset = `${'-' | '+'}${Digit}${Digit}${Digit}${Digit}`
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents a STANDARD or DAYLIGHT subcomponent, defining a time zone offset.
|
|
12
|
+
*/
|
|
13
|
+
export class TimeZoneOffset extends Component {
|
|
14
|
+
/**
|
|
15
|
+
* @param type If this is a STANDARD or DAYLIGHT component.
|
|
16
|
+
* @param start From when this offset is active.
|
|
17
|
+
* @param offsetFrom The offset that is in use prior to this time zone observance.
|
|
18
|
+
* @param offsetTo The offset that is in use during this time zone observance.
|
|
19
|
+
*/
|
|
20
|
+
constructor(
|
|
21
|
+
type: OffsetType,
|
|
22
|
+
start: CalendarDateOrTime | Date,
|
|
23
|
+
offsetFrom: Offset,
|
|
24
|
+
offsetTo: Offset
|
|
25
|
+
)
|
|
26
|
+
constructor(component: Component)
|
|
27
|
+
constructor(
|
|
28
|
+
a: OffsetType | Component,
|
|
29
|
+
b?: CalendarDateOrTime | Date,
|
|
30
|
+
c?: Offset,
|
|
31
|
+
d?: Offset
|
|
32
|
+
) {
|
|
33
|
+
let component: Component
|
|
34
|
+
if (a instanceof Component) {
|
|
35
|
+
component = a as Component
|
|
36
|
+
TimeZoneOffset.prototype.validate.call(component)
|
|
37
|
+
} else {
|
|
38
|
+
const name = a as OffsetType
|
|
39
|
+
const start = convertDate(b!)
|
|
40
|
+
const offsetFrom = c as Offset
|
|
41
|
+
const offsetTo = d as Offset
|
|
42
|
+
component = new Component(name)
|
|
43
|
+
component.setProperty('DTSTART', start)
|
|
44
|
+
component.setProperty('TZOFFSETFROM', offsetFrom)
|
|
45
|
+
component.setProperty('TZOFFSETTO', offsetTo)
|
|
46
|
+
}
|
|
47
|
+
super(component.name, component.properties, component.components)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
validate(): void {
|
|
51
|
+
if (!knownOffsetTypes.includes(this.name)) {
|
|
52
|
+
throw new ComponentValidationError(
|
|
53
|
+
'Component name must be STANDARD or DAYLIGHT'
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
const requiredProperties: AllowedPropertyName[] = [
|
|
57
|
+
'DTSTART',
|
|
58
|
+
'TZOFFSETFROM',
|
|
59
|
+
'TZOFFSETTO',
|
|
60
|
+
]
|
|
61
|
+
this.validateAllProperties(requiredProperties)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the date or time when this time zone offset starts.
|
|
66
|
+
* @returns The start date or time of this time zone offset.
|
|
67
|
+
*/
|
|
68
|
+
getStart(): CalendarDateOrTime {
|
|
69
|
+
return parseDateProperty(this.getProperty('DTSTART')!)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set the date or time when this time zone offset starts.
|
|
74
|
+
* @param value The start date or time.
|
|
75
|
+
* @returns The TimeZoneOffset instance for chaining.
|
|
76
|
+
*/
|
|
77
|
+
setStart(value: CalendarDateOrTime | Date): this {
|
|
78
|
+
return this.setProperty('DTSTART', convertDate(value))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the offset which is offset from during this time zone offset.
|
|
83
|
+
* @returns The offset in a format such as "+0100" or "-0230".
|
|
84
|
+
*/
|
|
85
|
+
getOffsetFrom(): Offset {
|
|
86
|
+
return this.getProperty('TZOFFSETFROM')!.value as Offset
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set the offset to offset from during this time zone offset.
|
|
91
|
+
* @param value An offset such as "+0100" or "-0230".
|
|
92
|
+
* @returns The TimeZoneOffset instance for chaining.
|
|
93
|
+
*/
|
|
94
|
+
setOffsetFrom(value: Offset): this {
|
|
95
|
+
return this.setProperty('TZOFFSETFROM', value)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the offset which is offset to during this time zone offset.
|
|
100
|
+
* @returns The offset in a format such as "+0100" or "-0230".
|
|
101
|
+
*/
|
|
102
|
+
getOffsetTo(): Offset {
|
|
103
|
+
return this.getProperty('TZOFFSETTO')!.value as Offset
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set the offset to offset to during this time zone offset.
|
|
108
|
+
* @param value An offset such as "+0100" or "-0230".
|
|
109
|
+
* @returns The TimeZoneOffset instance for chaining.
|
|
110
|
+
*/
|
|
111
|
+
setOffsetTo(value: Offset): this {
|
|
112
|
+
return this.setProperty('TZOFFSETTO', value)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getComment(): string | undefined {
|
|
116
|
+
return this.getProperty('COMMENT')?.value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setComment(value: string): this {
|
|
120
|
+
return this.setProperty('COMMENT', value)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
removeComment() {
|
|
124
|
+
this.removePropertiesWithName('COMMENT')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the name of this time zone offset.
|
|
129
|
+
* @returns The time zone offset name if it exists.
|
|
130
|
+
*/
|
|
131
|
+
getTimeZoneName(): string | undefined {
|
|
132
|
+
return this.getProperty('TZNAME')?.value
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Set the name of this time zone offset.
|
|
137
|
+
* @param value The new name.
|
|
138
|
+
* @returns The TimeZoneOffset instance for chaining.
|
|
139
|
+
* @example
|
|
140
|
+
* timeZoneOffset.setTimeZoneName('EST')
|
|
141
|
+
*/
|
|
142
|
+
setTimeZoneName(value: string): this {
|
|
143
|
+
return this.setProperty('TZNAME', value)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Remove the name of this time zone offset.
|
|
148
|
+
*/
|
|
149
|
+
removeTimeZoneName() {
|
|
150
|
+
this.removePropertiesWithName('TZNAME')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* eslint-disable */
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the date or time when this time zone offset starts.
|
|
157
|
+
* @returns The start date or time of this time zone offset.
|
|
158
|
+
* @deprecated Use {@link getStart} instead.
|
|
159
|
+
*/
|
|
160
|
+
start = this.getStart
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the offset which is offset from during this time zone offset.
|
|
164
|
+
* @returns The offset in a format such as "+0100" or "-0230".
|
|
165
|
+
* @deprecated Use {@link getOffsetFrom} instead.
|
|
166
|
+
*/
|
|
167
|
+
offsetFrom = this.getOffsetFrom
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get the offset which is offset to during this time zone offset.
|
|
171
|
+
* @returns The offset in a format such as "+0100" or "-0230".
|
|
172
|
+
* @deprecated Use {@link getOffsetTo} instead.
|
|
173
|
+
*/
|
|
174
|
+
offsetTo = this.getOffsetTo
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @deprecated Use {@link getComment} instead.
|
|
178
|
+
*/
|
|
179
|
+
comment = this.getComment
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the name of this time zone offset.
|
|
183
|
+
* @returns The time zone offset name if it exists.
|
|
184
|
+
* @deprecated Use {@link getTimeZoneName} instead.
|
|
185
|
+
*/
|
|
186
|
+
timeZoneName = this.getTimeZoneName
|
|
187
|
+
}
|
package/src/components/index.ts
CHANGED
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
|
+
}
|
package/src/index.ts
CHANGED
package/src/io.ts
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import readline from 'readline'
|
|
3
3
|
import { Calendar } from './components/Calendar'
|
|
4
|
-
import {
|
|
4
|
+
import { DeserializationError, deserializeComponent } from './parse'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Read a calendar from a
|
|
8
|
-
* @param path Path to the file
|
|
9
|
-
* @
|
|
10
|
-
* @throws
|
|
7
|
+
* Read a calendar from a iCalendar file.
|
|
8
|
+
* @param path Path to the file.
|
|
9
|
+
* @returns The calendar deserialized from the file.
|
|
10
|
+
* @throws {DeserializationError} If the file content is not a valid calendar.
|
|
11
11
|
*/
|
|
12
12
|
export async function load(path: fs.PathLike): Promise<Calendar> {
|
|
13
13
|
const stream = fs.createReadStream(path)
|
|
14
|
-
const lines = readline.createInterface({
|
|
14
|
+
const lines = readline.createInterface({
|
|
15
|
+
input: stream,
|
|
16
|
+
crlfDelay: Infinity,
|
|
17
|
+
})
|
|
15
18
|
|
|
16
|
-
const component = await
|
|
19
|
+
const component = await deserializeComponent(lines)
|
|
17
20
|
|
|
18
21
|
if (component.name != 'VCALENDAR') {
|
|
19
|
-
throw
|
|
22
|
+
throw new DeserializationError('Component must be a VCALENDAR')
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
return new Calendar(component)
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
* Write a calendar to a .
|
|
27
|
-
* @param calendar The calendar
|
|
28
|
-
* @param path Path to the file to write
|
|
29
|
+
* Write a calendar to a file.
|
|
30
|
+
* @param calendar The calendar to write to file.
|
|
31
|
+
* @param path Path to the file to write.
|
|
32
|
+
* @example
|
|
33
|
+
* dump(myCalendar, 'calendar.ics')
|
|
29
34
|
*/
|
|
30
35
|
export function dump(calendar: Calendar, path: string): Promise<void> {
|
|
31
36
|
return new Promise(resolve => {
|