iamcal 3.1.0 → 4.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.
package/src/date.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import * as patterns from './patterns'
2
2
  import { Property } from './property/Property'
3
+ import { CalendarDuration } from './duration'
3
4
 
4
5
  export const ONE_SECOND_MS = 1000
5
6
  export const ONE_MINUTE_MS = 60 * ONE_SECOND_MS
6
7
  export const ONE_HOUR_MS = 60 * ONE_MINUTE_MS
7
8
  export const ONE_DAY_MS = 24 * ONE_HOUR_MS
9
+ export const ONE_WEEK_MS = 7 * ONE_DAY_MS
10
+
11
+ export const ONE_MINUTE_SECONDS = 60
12
+ export const ONE_HOUR_SECONDS = 60 * ONE_MINUTE_SECONDS
13
+ export const ONE_DAY_SECONDS = 24 * ONE_HOUR_SECONDS
14
+ export const ONE_WEEK_SECONDS = 7 * ONE_DAY_SECONDS
8
15
 
9
16
  export interface CalendarDateOrTime {
10
17
  /**
@@ -24,7 +31,14 @@ export interface CalendarDateOrTime {
24
31
  * Check if this date represents a full day, as opposed to a date-time.
25
32
  * @returns `true` if this object is a {@link CalendarDate}.
26
33
  */
27
- isFullDay(): boolean
34
+ isFullDay(): this is CalendarDate
35
+
36
+ /**
37
+ * Get a new time offset by a duration.
38
+ * @param duration The duration to offset the time by.
39
+ * @returns A new {@link CalendarDateOrTime} of the same type.
40
+ */
41
+ offset(duration: CalendarDuration): CalendarDateOrTime
28
42
  }
29
43
 
30
44
  /**
@@ -36,10 +50,10 @@ export class CalendarDate implements CalendarDateOrTime {
36
50
 
37
51
  constructor(date: Date | string | CalendarDateOrTime) {
38
52
  if (typeof date === 'object') {
39
- if (Object.prototype.toString.call(date) === '[object Date]') {
40
- this.date = date as Date
53
+ if (isDateObject(date)) {
54
+ this.date = date
41
55
  } else {
42
- this.date = (date as CalendarDateOrTime).getDate()
56
+ this.date = (date).getDate()
43
57
  }
44
58
  } else {
45
59
  try {
@@ -64,22 +78,53 @@ export class CalendarDate implements CalendarDateOrTime {
64
78
  return new Date(this.date)
65
79
  }
66
80
 
67
- isFullDay(): boolean {
81
+ isFullDay(): this is CalendarDate {
68
82
  return true
69
83
  }
84
+
85
+ /**
86
+ * Get a new date offset by a duration.
87
+ * @param duration The duration to offset the date by.
88
+ * @returns A new {@link CalendarDate} offset by the duration.
89
+ */
90
+ offset(duration: CalendarDuration): CalendarDate {
91
+ const offsetMs = duration.floor('D').inMilliseconds()
92
+ const ms = this.getDate().getTime()
93
+ const time = new Date(ms + offsetMs)
94
+ return new CalendarDate(time)
95
+ }
96
+
97
+ [Symbol.toPrimitive](hint: string): number | string | null {
98
+ if (hint === 'string') {
99
+ return this.getValue()
100
+ } else if (hint === 'number') {
101
+ return this.date.getTime()
102
+ }
103
+ return null
104
+ }
70
105
  }
71
106
 
72
107
  export class CalendarDateTime implements CalendarDateOrTime {
73
108
  private date: Date
109
+ private absolute: boolean
74
110
 
75
- constructor(date: Date | string | CalendarDateOrTime) {
111
+ constructor(date: Date | string | CalendarDateOrTime, absolute?: boolean) {
76
112
  if (typeof date === 'object') {
77
- if (Object.prototype.toString.call(date) === '[object Date]') {
78
- this.date = date as Date
113
+ if (isDateObject(date)) {
114
+ this.date = date
115
+ this.absolute = false
79
116
  } else {
80
- this.date = (date as CalendarDateOrTime).getDate()
117
+ this.date = date.getDate()
118
+ this.absolute = date.isFullDay() ? false : (date as CalendarDateTime).isAbsolute()
81
119
  }
82
120
  } else {
121
+ this.absolute = absolute ?? date.endsWith('Z')
122
+ if (date.endsWith('Z') && !this.absolute) {
123
+ date = date.substring(0, date.length - 1)
124
+ } else if (!date.endsWith('Z') && this.absolute) {
125
+ date += 'Z'
126
+ }
127
+
83
128
  try {
84
129
  this.date = parseDateTimeString(date)
85
130
  } catch {
@@ -90,19 +135,81 @@ export class CalendarDateTime implements CalendarDateOrTime {
90
135
  if (!this.date || isNaN(this.date.getTime())) {
91
136
  throw new Error('Invalid date provided')
92
137
  }
138
+
139
+ if (absolute !== undefined) {
140
+ this.absolute = absolute
141
+ }
93
142
  }
94
143
 
95
144
  getValue(): string {
96
- return toDateTimeString(this.date)
145
+ return this.absolute
146
+ ? toDateTimeStringUTC(this.date, this.date.getTimezoneOffset())
147
+ : toDateTimeString(this.date)
97
148
  }
98
149
 
99
150
  getDate(): Date {
100
151
  return new Date(this.date)
101
152
  }
102
153
 
103
- isFullDay(): boolean {
154
+ isFullDay(): this is CalendarDate {
104
155
  return false
105
156
  }
157
+
158
+ /**
159
+ * Check if this datetime is absolute, i.e. if it is represented in UTC.
160
+ * @returns If the datetime is absolute.
161
+ */
162
+ isAbsolute(): boolean {
163
+ return this.absolute
164
+ }
165
+
166
+ /**
167
+ * Get a new time offset by a duration.
168
+ * @param duration The duration to offset the time by.
169
+ * @returns A new {@link CalendarDateTime} offset by the duration.
170
+ */
171
+ offset(duration: CalendarDuration): CalendarDateTime {
172
+ const offsetMs = duration.inMilliseconds()
173
+ const ms = this.getDate().getTime()
174
+ const time = new Date(ms + offsetMs)
175
+ return new CalendarDateTime(time)
176
+ }
177
+
178
+ [Symbol.toPrimitive](hint: string): number | string | null {
179
+ if (hint === 'string') {
180
+ return this.getValue()
181
+ } else if (hint === 'number') {
182
+ return this.date.getTime()
183
+ }
184
+ return null
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Check if an object is a JavaScript `Date`.
190
+ * @param maybeDate The object to check.
191
+ * @returns `true` if the object is a `Date`, `false` otherwise.
192
+ */
193
+ export function isDateObject(maybeDate: unknown): maybeDate is Date {
194
+ return Object.prototype.toString.call(maybeDate) === '[object Date]'
195
+ }
196
+
197
+ /**
198
+ * Check if an object is a `CalendarDateOrTime`.
199
+ * @param maybeDate The object to check.
200
+ * @returns `true` if the object is a `CalendarDateOrTime`, `false` otherwise.
201
+ */
202
+ export function isCalendarDateOrTime(
203
+ maybeDate: unknown
204
+ ): maybeDate is CalendarDateOrTime {
205
+ return (
206
+ maybeDate != null &&
207
+ typeof maybeDate === "object" &&
208
+ typeof (maybeDate as CalendarDateOrTime).getValue === "function" &&
209
+ typeof (maybeDate as CalendarDateOrTime).getDate === "function" &&
210
+ typeof (maybeDate as CalendarDateOrTime).isFullDay === "function" &&
211
+ typeof (maybeDate as CalendarDateOrTime).offset === "function"
212
+ )
106
213
  }
107
214
 
108
215
  /**
@@ -370,10 +477,10 @@ export function convertDate(
370
477
  date: Date | CalendarDateOrTime,
371
478
  fullDay: boolean = false
372
479
  ): CalendarDateOrTime {
373
- if (Object.prototype.toString.call(date) === '[object Date]') {
374
- if (fullDay) return new CalendarDate(date as Date)
375
- else return new CalendarDateTime(date as Date)
480
+ if (isDateObject(date)) {
481
+ if (fullDay) return new CalendarDate(date)
482
+ else return new CalendarDateTime(date)
376
483
  } else {
377
- return date as CalendarDateOrTime
484
+ return date
378
485
  }
379
486
  }
@@ -0,0 +1,329 @@
1
+ import {
2
+ CalendarDateOrTime,
3
+ isDateObject,
4
+ isCalendarDateOrTime,
5
+ ONE_DAY_MS,
6
+ ONE_DAY_SECONDS,
7
+ ONE_HOUR_SECONDS,
8
+ ONE_MINUTE_SECONDS,
9
+ ONE_SECOND_MS,
10
+ ONE_WEEK_SECONDS,
11
+ } from './date'
12
+ import { validateDuration } from './property'
13
+
14
+ export type DurationUnit = 'W' | 'D' | 'H' | 'M' | 'S'
15
+
16
+ /**
17
+ * Represents a DURATION value as defined by RFC5545.
18
+ */
19
+ export class CalendarDuration {
20
+ weeks?: number
21
+ days?: number
22
+ hours?: number
23
+ minutes?: number
24
+ seconds?: number
25
+
26
+ constructor(duration: string | CalendarDuration) {
27
+ if (typeof duration === 'string') {
28
+ validateDuration(duration)
29
+
30
+ const negativeMultiplier = duration.startsWith('-') ? -1 : 1
31
+ const findParts = /(\d+)([WDHMS])/g
32
+ const parts = duration.matchAll(findParts)
33
+ for (const part of parts) {
34
+ const value = parseInt(part[1], 10)
35
+ const name = part[2] as DurationUnit
36
+ switch (name) {
37
+ case 'W':
38
+ this.weeks = value * negativeMultiplier
39
+ break
40
+ case 'D':
41
+ this.days = value * negativeMultiplier
42
+ break
43
+ case 'H':
44
+ this.hours = value * negativeMultiplier
45
+ break
46
+ case 'M':
47
+ this.minutes = value * negativeMultiplier
48
+ break
49
+ case 'S':
50
+ this.seconds = value * negativeMultiplier
51
+ break
52
+ }
53
+ }
54
+ } else {
55
+ this.weeks = duration.weeks
56
+ this.days = duration.days
57
+ this.hours = duration.hours
58
+ this.minutes = duration.minutes
59
+ this.seconds = duration.seconds
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get the length of this duration object in seconds.
65
+ * @returns The total number of seconds represented by this duration.
66
+ */
67
+ inSeconds(): number {
68
+ return (
69
+ (this.weeks ?? 0) * ONE_WEEK_SECONDS +
70
+ (this.days ?? 0) * ONE_DAY_SECONDS +
71
+ (this.hours ?? 0) * ONE_HOUR_SECONDS +
72
+ (this.minutes ?? 0) * ONE_MINUTE_SECONDS +
73
+ (this.seconds ?? 0)
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Get the length of this duration object in milliseconds.
79
+ *
80
+ * Note that this will always return whole seconds, as durations do not
81
+ * support milliseconds.
82
+ * @returns The total number of milliseconds represented by this duration.
83
+ */
84
+ inMilliseconds(): number {
85
+ return this.inSeconds() * ONE_SECOND_MS
86
+ }
87
+
88
+ /**
89
+ * Get the duration string that represents this duration.
90
+ * @returns A duration string in the format `P[n]W` for weeks, or `P[n]DT[n]H[n]M[n]S` for days, hours, minutes and seconds. Prefixed with a `-` if negative.
91
+ */
92
+ getValue(): string {
93
+ return formatDurationString(
94
+ this.weeks,
95
+ this.days,
96
+ this.hours,
97
+ this.minutes,
98
+ this.seconds
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Get the floor the duration to the nearest of a given unit.
104
+ * @param unit The unit to floor to.
105
+ * @returns The duration with units smaller than `unit` removed.
106
+ * @example
107
+ * const duration = new CalendarDuration("1W2D5H30M")
108
+ * const days = duration.floor("D") // Floor to days
109
+ * console.log(days.getValue()) // "1W2D"
110
+ */
111
+ floor(unit: DurationUnit): CalendarDuration {
112
+ const result = new CalendarDuration(this)
113
+ /* eslint-disable no-fallthrough */
114
+ switch (unit) {
115
+ case 'W':
116
+ result.days = undefined
117
+ case 'D':
118
+ result.hours = undefined
119
+ case 'H':
120
+ result.minutes = undefined
121
+ case 'M':
122
+ result.seconds = undefined
123
+ }
124
+ /* eslint-enable no-fallthrough */
125
+ return result
126
+ }
127
+
128
+ /**
129
+ * Create a duration from a number of seconds.
130
+ * @param seconds How many seconds the duration should represent.
131
+ * @returns A {@link CalendarDuration} representing the specified number of seconds.
132
+ * @throws {Error} If seconds is NaN.
133
+ */
134
+ static fromSeconds(seconds: number): CalendarDuration {
135
+ return new CalendarDuration(secondsToDurationString(seconds))
136
+ }
137
+
138
+ /**
139
+ * Create a duration from a number of days.
140
+ * @param days How many days the duration should represent.
141
+ * @returns A {@link CalendarDuration} representing the specified number of days.
142
+ * @throws {Error} If days is NaN.
143
+ */
144
+ static fromDays(days: number): CalendarDuration {
145
+ return new CalendarDuration(daysToDurationString(days))
146
+ }
147
+
148
+ /**
149
+ * Create a duration from a number of weeks.
150
+ * @param weeks How many weeks the duration should represent.
151
+ * @returns A {@link CalendarDuration} representing the specified number of weeks.
152
+ * @throws {Error} If weeks is NaN.
153
+ */
154
+ static fromWeeks(weeks: number): CalendarDuration {
155
+ return new CalendarDuration(weeksToDurationString(weeks))
156
+ }
157
+
158
+ /**
159
+ * Creates a duration from the difference between two dates.
160
+ * @param start The start date or time.
161
+ * @param end The end date or time.
162
+ * @returns A {@link CalendarDuration} representing the difference between the two dates.
163
+ * @throws {Error} If the start date and end date have different types.
164
+ */
165
+ static fromDifference(
166
+ start: Date | CalendarDateOrTime,
167
+ end: Date | CalendarDateOrTime
168
+ ): CalendarDuration {
169
+ if (isDateObject(start)) {
170
+ // Date
171
+ if (isDateObject(end)) {
172
+ const differenceSeconds =
173
+ (end.getTime() - start.getTime()) / ONE_SECOND_MS
174
+ return CalendarDuration.fromSeconds(differenceSeconds)
175
+ }
176
+ } else if (start.isFullDay()) {
177
+ // CalendarDate
178
+ if (isCalendarDateOrTime(end) && end.isFullDay()) {
179
+ const startMs = start.getDate().getTime()
180
+ const endMs = end.getDate().getTime()
181
+ const differenceDays = (endMs - startMs) / ONE_DAY_MS
182
+ return CalendarDuration.fromDays(differenceDays)
183
+ }
184
+ } else {
185
+ // CalendarDateTime
186
+ if (isCalendarDateOrTime(end) && !end.isFullDay()) {
187
+ const startMs = start.getDate().getTime()
188
+ const endMs = end.getDate().getTime()
189
+ const differenceSeconds = (endMs - startMs) / ONE_SECOND_MS
190
+ return CalendarDuration.fromSeconds(differenceSeconds)
191
+ }
192
+ }
193
+
194
+ throw new Error('Start and end dates must be of the same type')
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Convert time units to a duration string.
200
+ *
201
+ * A duration is considered negative if any unit is less than 0.
202
+ * @param weeks How many weeks the duration should represent.
203
+ * @param days The days part of the duration.
204
+ * @param hours The hours part of the duration.
205
+ * @param minutes The minutes part of the duration.
206
+ * @param seconds The seconds part of the duration.
207
+ * @returns A string representing the duration in the format `P[n]W` or `P[n]DT[n]H[n]M[n]S` where values may be omitted if 0, prefixed with `-` if negative.
208
+ * @throws {Error} If weeks are combined with other values.
209
+ * @throws {Error} If any unit is NaN.
210
+ */
211
+ export function formatDurationString(
212
+ weeks: number | undefined,
213
+ days: number | undefined,
214
+ hours: number | undefined,
215
+ minutes: number | undefined,
216
+ seconds: number | undefined,
217
+ ): string {
218
+ let prefix = ''
219
+ let durationString = prefix + 'P'
220
+ let hasTime = false
221
+ let isEmpty = true
222
+
223
+ const appendUnit = (time: number | undefined, unit: string) => {
224
+ if (time === undefined) return
225
+ if (isNaN(time)) {
226
+ throw new Error(`${unit} must not be NaN`)
227
+ }
228
+ if (time < 0) prefix = '-'
229
+ const absTime = Math.abs(Math.floor(time))
230
+ durationString += absTime + unit.charAt(0)
231
+ isEmpty = false
232
+ }
233
+ const appendTimeUnit = (time: number | undefined, unit: string) => {
234
+ if (time === undefined) return
235
+ if (!hasTime) {
236
+ durationString += 'T'
237
+ hasTime = true
238
+ }
239
+ appendUnit(time, unit)
240
+ }
241
+
242
+ // Add units
243
+ if (weeks !== undefined) {
244
+ if (days !== undefined ||
245
+ hours !== undefined ||
246
+ minutes !== undefined ||
247
+ seconds !== undefined) {
248
+ throw new Error('Cannot combine weeks with other units in duration string')
249
+ }
250
+
251
+ appendUnit(weeks, 'Weeks')
252
+ }
253
+ appendUnit(days, 'Days')
254
+ appendTimeUnit(hours, 'Hours')
255
+ if (minutes !== undefined) {
256
+ appendTimeUnit(minutes, 'Minutes')
257
+ } else if (hours !== undefined && seconds !== undefined) {
258
+ appendTimeUnit(0, 'Minutes')
259
+ }
260
+ appendTimeUnit(seconds, 'Seconds')
261
+
262
+ if (isEmpty) {
263
+ throw new Error('Duration string must not be empty')
264
+ }
265
+
266
+ return prefix + durationString
267
+ }
268
+
269
+ /**
270
+ * Convert a number of seconds to a duration string.
271
+ * @param seconds How many seconds the duration should represent.
272
+ * @returns A string representing the duration in the format `PT[n]H[n]M[n]S` where values may be omitted if 0, prefixed with `-` if negative.
273
+ * @throws {Error} If seconds is NaN.
274
+ */
275
+ export function secondsToDurationString(seconds: number): string {
276
+ if (isNaN(seconds)) throw new Error('Seconds must not be NaN')
277
+ seconds = Math.floor(seconds)
278
+
279
+ const absSeconds = Math.abs(seconds)
280
+ const minutes = Math.floor(absSeconds / 60)
281
+ const hours = Math.floor(minutes / 60)
282
+ let secondsValue: number | undefined = seconds % 60
283
+ let minutesValue: number | undefined = minutes % 60
284
+ let hoursValue: number | undefined = hours
285
+
286
+ if (hoursValue === 0) hoursValue = undefined
287
+ if (minutesValue === 0) minutesValue = undefined
288
+ if (secondsValue === 0 && !(minutesValue === undefined && hoursValue === undefined)) secondsValue = undefined
289
+
290
+ return formatDurationString(
291
+ undefined,
292
+ undefined,
293
+ hoursValue,
294
+ minutesValue,
295
+ secondsValue,
296
+ )
297
+ }
298
+
299
+ /**
300
+ * Convert a number of days to a duration string.
301
+ * @param days How many days the duration should represent.
302
+ * @returns A string representing the duration in the format `P[n]D`, prefixed with `-` if negative.
303
+ * @throws {Error} If days is NaN.
304
+ */
305
+ export function daysToDurationString(days: number): string {
306
+ return formatDurationString(
307
+ undefined,
308
+ days,
309
+ undefined,
310
+ undefined,
311
+ undefined
312
+ )
313
+ }
314
+
315
+ /**
316
+ * Convert a number of weeks to a duration string.
317
+ * @param weeks How many weeks the duration should represent.
318
+ * @returns A string representing the duration in the format `P[n]W`, prefixed with `-` if negative.
319
+ * @throws {Error} If weeks is NaN.
320
+ */
321
+ export function weeksToDurationString(weeks: number): string {
322
+ return formatDurationString(
323
+ weeks,
324
+ undefined,
325
+ undefined,
326
+ undefined,
327
+ undefined
328
+ )
329
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './component'
2
2
  export * from './components'
3
3
  export * from './date'
4
+ export * from './duration'
4
5
  export * from './io'
5
6
  export * from './parse'
6
7
  export * from './property'
@@ -1,4 +1,5 @@
1
- import { CalendarDateOrTime } from 'src/date'
1
+ import { CalendarDateOrTime, CalendarDate, CalendarDateTime, isCalendarDateOrTime } from '../date'
2
+ import { CalendarDuration } from '../duration'
2
3
  import {
3
4
  escapePropertyParameterValue,
4
5
  escapeTextPropertyValue,
@@ -16,7 +17,7 @@ import {
16
17
  RsvpExpectation,
17
18
  } from './parameter'
18
19
  import { validateCalendarUserAddress, validateContentType } from './validate'
19
- import { AllowedValueType, getDefaultValueType } from './valueType'
20
+ import { AllowedValueType, KnownValueType, getDefaultValueType } from './valueType'
20
21
 
21
22
  /**
22
23
  * Represents a property of a calendar component as described by RFC 5545 in
@@ -55,13 +56,72 @@ export class Property {
55
56
  }
56
57
 
57
58
  static fromDate(name: string, value: CalendarDateOrTime): Property {
59
+ const valueType = value.isFullDay() ? 'DATE' : 'DATE-TIME'
60
+ let properties: { [k: string]: string | string[] } | undefined = undefined
61
+ if (getDefaultValueType(name) !== valueType) {
62
+ properties = { VALUE: valueType }
63
+ }
64
+
65
+ return new Property(
66
+ name,
67
+ value.getValue(),
68
+ properties,
69
+ )
70
+ }
71
+
72
+ static fromDuration(name: string, value: CalendarDuration): Property {
58
73
  return new Property(
59
74
  name,
60
75
  value.getValue(),
61
- value.isFullDay() ? { VALUE: 'DATE' } : undefined
76
+ { VALUE: 'CALENDARDATE' },
62
77
  )
63
78
  }
64
79
 
80
+ /**
81
+ * Get the value of this property, converted into the appropriate class if possible.
82
+ * @returns The value as a string or appropriate class.
83
+ */
84
+ getValue(): string | CalendarDateOrTime | CalendarDuration {
85
+ const valueType = this.getValueType()
86
+ switch (valueType) {
87
+ case 'DATE':
88
+ return new CalendarDate(this.value)
89
+ case 'DATE-TIME':
90
+ return new CalendarDateTime(this.value)
91
+ case 'DURATION':
92
+ return new CalendarDuration(this.value)
93
+ default:
94
+ return this.value
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Set the value of this property and change the value type if a class is used.
100
+ * @param value The new value as a string or class.
101
+ */
102
+ setValue(value: string | CalendarDateOrTime | CalendarDuration) {
103
+ const valueTypeBefore = this.getExplicitValueType()
104
+ if (typeof value === 'string') {
105
+ // Set string
106
+ this.value = value
107
+ } else if (isCalendarDateOrTime(value)) {
108
+ // Set date
109
+ this.value = value.getValue()
110
+ this.setValueType(value.isFullDay() ? 'DATE' : 'DATE-TIME')
111
+ } else {
112
+ // Set duration
113
+ this.value = value.getValue()
114
+ this.setValueType('DURATION')
115
+ }
116
+
117
+ // Remove value type if set to default, unless it was already set
118
+ const valueType = this.getExplicitValueType()
119
+ const defaultValueType = this.getDefaultValueType()
120
+ if (valueType === defaultValueType && valueType !== valueTypeBefore) {
121
+ this.removeValueType()
122
+ }
123
+ }
124
+
65
125
  setParameter(name: string, value: string | string[]) {
66
126
  this._parameters.set(
67
127
  name.toUpperCase(),
@@ -148,8 +208,23 @@ export class Property {
148
208
  * @returns The value type of this property.
149
209
  */
150
210
  getValueType(): AllowedValueType {
151
- const assignedValueType = this.getParameter('VALUE')?.[0].toUpperCase()
152
- return assignedValueType ?? getDefaultValueType(this.name)
211
+ return this.getExplicitValueType() ?? this.getDefaultValueType()
212
+ }
213
+
214
+ /**
215
+ * Get the value type of this property.
216
+ * @returns The value type of this property, or `undefined` if unset.
217
+ */
218
+ getExplicitValueType(): AllowedValueType | undefined {
219
+ return this.getParameter('VALUE')?.[0].toUpperCase()
220
+ }
221
+
222
+ /**
223
+ * Get the default value type for this property.
224
+ * @returns The default value type based on the name of this property.
225
+ */
226
+ getDefaultValueType(): KnownValueType {
227
+ return getDefaultValueType(this.name)
153
228
  }
154
229
 
155
230
  /**
@@ -85,7 +85,7 @@ export function validateDateTime(value: string) {
85
85
  parseDateTimeString(value)
86
86
  } catch {
87
87
  throw new PropertyValidationError(
88
- `${value} does not match pattern for DATETIME`
88
+ `${value} does not match pattern for DATE-TIME`
89
89
  )
90
90
  }
91
91
  }