-
Notifications
You must be signed in to change notification settings - Fork 233
/
Copy pathCalendarEvent.cs
348 lines (311 loc) · 11.8 KB
/
CalendarEvent.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//
// Copyright ical.net project maintainers and contributors.
// Licensed under the MIT license.
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Ical.Net.DataTypes;
using Ical.Net.Evaluation;
using Ical.Net.Utility;
namespace Ical.Net.CalendarComponents;
/// <summary>
/// A class that represents an RFC 5545 VEVENT component.
/// </summary>
/// <note>
/// TODO: Add support for the following properties:
/// <list type="bullet">
/// <item>Add support for the Organizer and Attendee properties</item>
/// <item>Add support for the Class property</item>
/// <item>Add support for the Geo property</item>
/// <item>Add support for the Priority property</item>
/// <item>Add support for the Related property</item>
/// <item>Create a TextCollection DataType for 'text' items separated by commas</item>
/// </list>
/// </note>
public class CalendarEvent : RecurringComponent, IAlarmContainer, IComparable<CalendarEvent>
{
internal const string ComponentName = "VEVENT";
/// <summary>
/// The end date/time of the event.
/// <note>
/// If the duration has not been set, but
/// the start/end time of the event is available,
/// the duration is automatically determined.
/// Likewise, if an end time and duration are available,
/// but a start time has not been set, the start time
/// will be extrapolated.
/// </note>
/// </summary>
public virtual IDateTime DtEnd
{
get => Properties.Get<IDateTime>("DTEND");
set
{
if (!Equals(DtEnd, value))
{
Properties.Set("DTEND", value);
}
}
}
/// <summary>
/// The duration of the event.
/// <note>
/// If a start time and duration is available,
/// the end time is automatically determined.
/// Likewise, if the end time and duration is
/// available, but a start time is not determined,
/// the start time will be extrapolated from
/// available information.
/// </note>
/// </summary>
// NOTE: Duration is not supported by all systems,
// (i.e. iPhone) and cannot co-exist with DtEnd.
// RFC 5545 states:
//
// ; either 'dtend' or 'duration' may appear in
// ; a 'eventprop', but 'dtend' and 'duration'
// ; MUST NOT occur in the same 'eventprop'
//
// Therefore, Duration is not serialized, as DtEnd
// should always be extrapolated from the duration.
public virtual TimeSpan Duration
{
get => Properties.Get<TimeSpan>("DURATION");
set
{
if (!Equals(Duration, value))
{
Properties.Set("DURATION", value);
}
}
}
/// <summary>
/// Calculates and returns the duration of the first occurrence of this event.
/// </summary>
/// <remarks>
/// If the 'DURATION' property is set, this method will return its value.
/// Otherwise, if DTSTART and DTEND are set, it will return DTSTART minus DTEND.
/// Otherwise it will return `default(TimeSpan)`.
/// Note that for recurring events, the duration of individual occurrences may vary
/// if they span a DST change.
/// </remarks>
/// <returns>The effective duration of this event.</returns>
public virtual TimeSpan GetFirstDuration()
{
if (Properties.ContainsKey("DURATION"))
return Duration;
if (DtStart is not null)
{
if (DtEnd is not null)
{
// The "DTEND" property
// for a "VEVENT" calendar component specifies the non-inclusive end
// of the event.
return DtEnd.Subtract(DtStart);
}
else if (!DtStart.HasTime)
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE value type but no
// "DTEND" nor "DURATION" property, the event's duration is taken to
// be one day.
return TimeSpan.FromDays(1);
}
else
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE-TIME value type but no
// "DTEND" property, the event ends on the same calendar date and
// time of day specified by the "DTSTART" property.
return TimeSpan.Zero;
}
}
// This is an illegal state. We return zero for compatibility reasons.
return TimeSpan.Zero;
}
/// <summary>
/// An alias to the DtEnd field (i.e. end date/time).
/// </summary>
public virtual IDateTime End
{
get => DtEnd;
set => DtEnd = value;
}
/// <summary>
/// Returns true if the event is an all-day event.
/// </summary>
public virtual bool IsAllDay
{
get => !Start.HasTime;
}
/// <summary>
/// The geographic location (lat/long) of the event.
/// </summary>
public GeographicLocation GeographicLocation
{
get => Properties.Get<GeographicLocation>("GEO");
set => Properties.Set("GEO", value);
}
/// <summary>
/// The location of the event.
/// </summary>
public string Location
{
get => Properties.Get<string>("LOCATION");
set => Properties.Set("LOCATION", value);
}
/// <summary>
/// Resources that will be used during the event.
/// To change existing values, assign a new <see cref="IList{T}"/>.
/// <example>Examples:
/// Conference room, Projector
/// </example>
/// </summary>
public virtual IList<string> Resources
{
get => Properties.GetMany<string>("RESOURCES");
set => Properties.Set("RESOURCES", value ?? new List<string>());
}
/// <summary>
/// The status of the event.
/// </summary>
public string Status
{
get => Properties.Get<string>("STATUS");
set => Properties.Set("STATUS", value);
}
/// <summary>
/// The transparency of the event. In other words,
/// whether the period of time this event
/// occupies can contain other events (transparent),
/// or if the time cannot be scheduled for anything
/// else (opaque).
/// </summary>
public string Transparency
{
get => Properties.Get<string>(TransparencyType.Key);
set => Properties.Set(TransparencyType.Key, value);
}
/// <summary>
/// Constructs an Event object, with an iCalObject
/// (usually an iCalendar object) as its parent.
/// </summary>
public CalendarEvent()
{
Initialize();
}
private void Initialize()
{
Name = EventStatus.Name;
SetService(new EventEvaluator(this));
}
/// <summary>
/// Determines whether the <see cref="CalendarEvent"/> is actively displayed
/// as an upcoming or occurred event.
/// </summary>
/// <returns>True if the event has not been cancelled, False otherwise.</returns>
public virtual bool IsActive => !string.Equals(Status, EventStatus.Cancelled, EventStatus.Comparison);
protected override bool EvaluationIncludesReferenceDate => true;
protected override void OnDeserializing(StreamingContext context)
{
base.OnDeserializing(context);
Initialize();
}
protected override void OnDeserialized(StreamingContext context)
{
base.OnDeserialized(context);
}
protected bool Equals(CalendarEvent other)
{
var resourcesSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
resourcesSet.UnionWith(Resources);
var result =
Equals(DtStart, other.DtStart)
&& string.Equals(Summary, other.Summary, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase)
&& Equals(DtEnd, other.DtEnd)
&& Equals(Duration, other.Duration)
&& string.Equals(Location, other.Location, StringComparison.OrdinalIgnoreCase)
&& resourcesSet.SetEquals(other.Resources)
&& string.Equals(Status, other.Status, StringComparison.Ordinal)
&& IsActive == other.IsActive
&& string.Equals(Transparency, other.Transparency, TransparencyType.Comparison)
&& EvaluationIncludesReferenceDate == other.EvaluationIncludesReferenceDate
&& Attachments.SequenceEqual(other.Attachments)
&& CollectionHelpers.Equals(ExceptionRules, other.ExceptionRules)
&& CollectionHelpers.Equals(RecurrenceRules, other.RecurrenceRules);
if (!result)
{
return false;
}
//RDATEs and EXDATEs are all List<PeriodList>, because the spec allows for multiple declarations of collections.
//Consequently we have to contrive a normalized representation before we can determine whether two events are equal
var exDates = PeriodList.GetGroupedPeriods(ExceptionDates);
var otherExDates = PeriodList.GetGroupedPeriods(other.ExceptionDates);
if (exDates.Keys.Count != otherExDates.Keys.Count || !exDates.Keys.OrderBy(k => k).SequenceEqual(otherExDates.Keys.OrderBy(k => k)))
{
return false;
}
if (exDates.Any(exDate => !exDate.Value.OrderBy(d => d).SequenceEqual(otherExDates[exDate.Key].OrderBy(d => d))))
{
return false;
}
var rDates = PeriodList.GetGroupedPeriods(RecurrenceDates);
var otherRDates = PeriodList.GetGroupedPeriods(other.RecurrenceDates);
if (rDates.Keys.Count != otherRDates.Keys.Count || !rDates.Keys.OrderBy(k => k).SequenceEqual(otherRDates.Keys.OrderBy(k => k)))
{
return false;
}
if (rDates.Any(exDate => !exDate.Value.OrderBy(d => d).SequenceEqual(otherRDates[exDate.Key].OrderBy(d => d))))
{
return false;
}
return true;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((CalendarEvent)obj);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = DtStart?.GetHashCode() ?? 0;
hashCode = (hashCode * 397) ^ (Summary?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (Description?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (DtEnd?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ Duration.GetHashCode();
hashCode = (hashCode * 397) ^ (Location?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ Status?.GetHashCode() ?? 0;
hashCode = (hashCode * 397) ^ IsActive.GetHashCode();
hashCode = (hashCode * 397) ^ Transparency?.GetHashCode() ?? 0;
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(Attachments);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(Resources);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCodeForNestedCollection(ExceptionDates);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(ExceptionRules);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCodeForNestedCollection(RecurrenceDates);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(RecurrenceRules);
return hashCode;
}
}
public int CompareTo(CalendarEvent other)
{
if (DtStart.Equals(other.DtStart))
{
return 0;
}
if (DtStart.LessThan(other.DtStart))
{
return -1;
}
if (DtStart.GreaterThan(other.DtStart))
{
return 1;
}
throw new Exception("An error occurred while comparing two CalDateTimes.");
}
}