1- import { Options , Frequency } from './types'
1+ import { Options , Frequency , DateTimeProperty , DateTimeValue } from './types'
22import { Weekday } from './weekday'
33import dateutil from './dateutil'
44import { Days } from './rrule'
55
66export function parseString ( rfcString : string ) : Partial < Options > {
77 const options = rfcString . split ( '\n' ) . map ( parseLine ) . filter ( x => x !== null )
8- return options . reduce ( ( acc , cur ) => Object . assign ( acc , cur ) , { } ) || { }
8+ /**
9+ * From [RFC 5545](https://tools.ietf.org/html/rfc5545):
10+ *
11+ * 3.8.2.2. Date-Time End ("DTEND")
12+ *
13+ * The value type of this property MUST be the same as the "DTSTART" property, and its
14+ * value MUST be later in time than the value of the "DTSTART" property. Furthermore,
15+ * this property MUST be specified as a date with local time if and only if the
16+ * "DTSTART" property is also specified as a date with local time.
17+ */
18+ return options . reduce ( ( acc : Partial < Options > , cur : Partial < Options > ) => {
19+ let existing
20+ if ( cur . dtstart ) {
21+ if ( acc . dtstart ) {
22+ throw new Error ( 'Invalid rule: DTSTART must occur only once' )
23+ }
24+ if ( acc . dtend && acc . dtend . valueOf ( ) <= cur . dtstart . valueOf ( ) ) {
25+ throw new Error ( 'Invalid rule: DTEND must be later than DTSTART' )
26+ }
27+ existing = acc . dtend
28+ }
29+ if ( cur . dtend ) {
30+ if ( acc . dtend ) {
31+ throw new Error ( 'Invalid rule: DTEND must occur only once' )
32+ }
33+ if ( acc . dtstart && acc . dtstart . valueOf ( ) >= cur . dtend . valueOf ( ) ) {
34+ throw new Error ( 'Invalid rule: DTEND must be later than DTSTART' )
35+ }
36+ existing = acc . dtstart
37+ }
38+ if ( existing && acc . dtvalue !== cur . dtvalue ) {
39+ // Different value types.
40+ throw new Error ( 'Invalid rule: DTSTART and DTEND must have the same value type' )
41+ } else if ( existing && acc . tzid !== cur . tzid ) {
42+ // Different timezones.
43+ throw new Error ( 'Invalid rule: DTSTART and DTEND must have the same timezone' )
44+ } else if ( existing && acc . dtfloating !== cur . dtfloating ) {
45+ // Different floating types.
46+ throw new Error ( 'Invalid rule: DTSTART and DTEND must both be floating' )
47+ }
48+ return Object . assign ( acc , cur )
49+ } , { } ) || { }
950}
1051
11- export function parseDateTime ( line : string , end : boolean = false ) {
52+ export function parseDateTime ( line : string , prop = DateTimeProperty . START ) : Partial < Options > {
1253 const options : Partial < Options > = { }
1354
14- const dtWithZone = end
15- ? / D T E N D (?: ; T Z I D = ( [ ^ : = ] + ?) ) ? (?: : | = ) ( [ ^ ; \s ] + ) / i . exec ( line )
16- : / D T S T A R T (?: ; T Z I D = ( [ ^ : = ] + ? ) ) ? (?: : | = ) ( [ ^ ; \s ] + ) / i . exec ( line )
55+ const dtWithZone = new RegExp (
56+ ` ${ prop } (?:;TZID=([^:=]+?))?(?:;VALUE=(DATE|DATE-TIME))?(?:: |=)([^;\\ s]+)` , 'i'
57+ ) . exec ( line )
1758
1859 if ( ! dtWithZone ) {
1960 return options
2061 }
2162
22- const [ _ , tzid , dt ] = dtWithZone
63+ const [ _ , tzid , dtvalue , dt ] = dtWithZone
2364
2465 if ( tzid ) {
66+ if ( dt . endsWith ( 'Z' ) ) {
67+ throw new Error ( `Invalid UTC date-time with timezone: ${ line } ` )
68+ }
2569 options . tzid = tzid
2670 }
27- if ( end ) {
28- options . dtend = dateutil . untilStringToDate ( dt )
29- } else {
30- options . dtstart = dateutil . untilStringToDate ( dt )
71+
72+ if ( dtvalue === DateTimeValue . DATE ) {
73+ if ( prop === DateTimeProperty . START ) {
74+ options . dtstart = dateutil . fromRfc5545Date ( dt )
75+ } else {
76+ options . dtend = dateutil . fromRfc5545Date ( dt )
77+ }
78+ options . dtvalue = DateTimeValue . DATE
79+ options . dtfloating = true
80+ } else { // Default value type is DATE-TIME
81+ if ( prop === DateTimeProperty . START ) {
82+ options . dtstart = dateutil . fromRfc5545DateTime ( dt )
83+ } else {
84+ options . dtend = dateutil . fromRfc5545DateTime ( dt )
85+ }
86+ if ( dtvalue ) {
87+ options . dtvalue = DateTimeValue . DATE_TIME
88+ }
89+ if ( ! tzid && ! dt . endsWith ( 'Z' ) ) {
90+ options . dtfloating = ! tzid && ! dt . endsWith ( 'Z' )
91+ }
3192 }
93+
3294 return options
3395}
3496
@@ -47,9 +109,9 @@ function parseLine (rfcString: string) {
47109 case 'EXRULE' :
48110 return parseRrule ( rfcString )
49111 case 'DTSTART' :
50- return parseDateTime ( rfcString )
112+ return parseDateTime ( rfcString , DateTimeProperty . START )
51113 case 'DTEND' :
52- return parseDateTime ( rfcString , true /* end */ )
114+ return parseDateTime ( rfcString , DateTimeProperty . END )
53115 default :
54116 throw new Error ( `Unsupported RFC prop ${ key } in ${ rfcString } ` )
55117 }
@@ -95,9 +157,15 @@ function parseRrule (line: string) {
95157 const dtstart = parseDateTime ( line )
96158 options . tzid = dtstart . tzid
97159 options . dtstart = dtstart . dtstart
160+ if ( dtstart . dtvalue ) {
161+ options . dtvalue = dtstart . dtvalue
162+ }
163+ if ( dtstart . dtfloating ) {
164+ options . dtfloating = dtstart . dtfloating
165+ }
98166 break
99167 case 'UNTIL' :
100- options . until = dateutil . untilStringToDate ( value )
168+ options . until = dateutil . fromRfc5545DateTime ( value )
101169 break
102170 case 'BYEASTER' :
103171 options . byeaster = Number ( value )
0 commit comments