1 /++
2
3 OpenD could use automatic mixin to child class...
4
5 Extensions: color. exrule? trash day - if holiday occurred that week, move it forward a day
6
7 Standards: categories
8
9 UI idea for rrule: show a mini two year block with the day highlighted
10 -> also just let user click on a bunch of days so they can make a list
11
12 Want ability to add special info to a single item of a recurring event
13
14 Can use inotify to reload ui when sqlite db changes (or a trigger on postgres?)
15
16 https://datatracker.ietf.org/doc/html/rfc5545
17 https://icalendar.org/
18 +/
19 module arsd.calendar;
20
21 import arsd.core;
22
23 import std.datetime;
24
25 /++
26 History:
27 Added July 3, 2024
28 +/
29 SimplifiedUtcTimestamp parseTimestampString(string when, SysTime relativeTo) /*pure*/ {
30 import std.string;
31
32 int parsingWhat;
33 int bufferedNumber = int.max;
34
35 int secondsCount;
36
37 void addSeconds(string word, int bufferedNumber, int multiplier) {
38 if(parsingWhat == 0)
39 parsingWhat = 1;
40 if(parsingWhat != 1)
41 throw ArsdException!"unusable timestamp string"("you said 'at' but gave a relative time", when);
42 if(bufferedNumber == int.max)
43 throw ArsdException!"unusable timestamp string"("no number before unit", when, word);
44 secondsCount += bufferedNumber * multiplier;
45 bufferedNumber = int.max;
46 }
47
48 foreach(word; when.split(" ")) {
49 word = strip(word).toLower().replace(",", "");
50 if(word == "in")
51 parsingWhat = 1;
52 else if(word == "at")
53 parsingWhat = 2;
54 else if(word == "and") {
55 // intentionally blank
56 } else if(word.indexOf(":") != -1) {
57 if(secondsCount != 0)
58 throw ArsdException!"unusable timestamp string"("cannot mix time styles", when, word);
59
60 if(parsingWhat == 0)
61 parsingWhat = 2; // assume absolute time when this comes in
62
63 bool wasPm;
64
65 if(word.length > 2 && word[$-2 .. $] == "pm") {
66 word = word[0 .. $-2];
67 wasPm = true;
68 } else if(word.length > 2 && word[$-2 .. $] == "am") {
69 word = word[0 .. $-2];
70 }
71
72 // FIXME: what about midnight?
73 int multiplier = 3600;
74 foreach(part; word.split(":")) {
75 import std.conv;
76 secondsCount += multiplier * to!int(part);
77 multiplier /= 60;
78 }
79
80 if(wasPm)
81 secondsCount += 12 * 3600;
82 } else if(word.isNumeric()) {
83 import std.conv;
84 bufferedNumber = to!int(word);
85 } else if(word == "seconds" || word == "second") {
86 addSeconds(word, bufferedNumber, 1);
87 } else if(word == "minutes" || word == "minute") {
88 addSeconds(word, bufferedNumber, 60);
89 } else if(word == "hours" || word == "hour") {
90 addSeconds(word, bufferedNumber, 60 * 60);
91 } else
92 throw ArsdException!"unusable timestamp string"("i dont know what this word means", when, word);
93 }
94
95 if(parsingWhat == 0)
96 throw ArsdException!"unusable timestamp string"("couldn't figure out what to do with this input", when);
97
98 else if(parsingWhat == 1) // relative time
99 return SimplifiedUtcTimestamp((relativeTo + seconds(secondsCount)).stdTime);
100 else if(parsingWhat == 2) { // absolute time (assuming it is today in our time zone)
101 auto today = relativeTo;
102 today.hour = 0;
103 today.minute = 0;
104 today.second = 0;
105 return SimplifiedUtcTimestamp((today + seconds(secondsCount)).stdTime);
106 } else
107 assert(0);
108 }
109
110 unittest {
111 auto testTime = SysTime(DateTime(Date(2024, 07, 03), TimeOfDay(10, 0, 0)), UTC());
112 void test(string what, string expected) {
113 auto result = parseTimestampString(what, testTime).toString;
114 assert(result == expected, result);
115 }
116
117 test("in 5 minutes", "2024-07-03T10:05:00Z");
118 test("in 5 minutes and 5 seconds", "2024-07-03T10:05:05Z");
119 test("in 5 minutes, 45 seconds", "2024-07-03T10:05:45Z");
120 test("at 5:44", "2024-07-03T05:44:00Z");
121 test("at 5:44pm", "2024-07-03T17:44:00Z");
122 }
123
124 version(none)
125 void main() {
126 auto e = new CalendarEvent(
127 start: DateTime(2024, 4, 22),
128 end: Date(2024, 04, 22),
129 );
130 }
131
132 class Calendar {
133 CalendarEvent[] events;
134 }
135
136 /++
137
138 +/
139 class CalendarEvent {
140 DateWithOptionalTime start;
141 DateWithOptionalTime end;
142
143 Recurrence recurrence;
144
145 int color;
146 string title; // summary
147 string details;
148
149 string uid;
150
151 this(DateWithOptionalTime start, DateWithOptionalTime end, Recurrence recurrence = Recurrence.none) {
152 this.start = start;
153 this.end = end;
154 this.recurrence = recurrence;
155 }
156 }
157
158 /++
159
160 +/
161 struct DateWithOptionalTime {
162 string tzlocation;
163 DateTime dt;
164 bool hadTime;
165
166 @implicit
167 this(DateTime dt) {
168 this.dt = dt;
169 this.hadTime = true;
170 }
171
172 @implicit
173 this(Date d) {
174 this.dt = DateTime(d, TimeOfDay.init);
175 this.hadTime = false;
176 }
177
178 this(in char[] s) {
179 // FIXME
180 }
181 }
182
183 /++
184
185 +/
186 struct Recurrence {
187 static Recurrence none() {
188 return Recurrence.init;
189 }
190 }
191
192 /+
193
194 enum FREQ {
195
196 }
197
198 struct RRULE {
199 FREQ freq;
200 int interval;
201 int count;
202 DAY wkst;
203
204 // these can be negative too indicating the xth from the last...
205 DAYSET byday; // ubyte bitmask... except it can also have numbers atached wtf
206
207 // so like `BYDAY=-2MO` means second-to-last monday
208
209 MONTHDAYSET byMonthDay; // uint bitmask
210 HOURSET byHour; // uint bitmask
211 MONTHDSET byMonth; // ushort bitmask
212
213 WEEKSET byWeekNo; // ulong bitmask
214
215 int BYSETPOS;
216 }
217
218 +/
219
220 struct ICalParser {
221 // if the following line starts with whitespace, remove the cr/lf/ and that ONE ws char, then add to the previous line
222 // it is supposed to support this even if it is in the middle of a utf-8 sequence
223 // contentline = name *(";" param ) ":" value CRLF
224 // you're supposed to split lines longer than 75 octets when generating.
225
226 void feedEntireFile(in ubyte[] data) {
227 feed(data);
228 feed(null);
229 }
230 void feedEntireFile(in char[] data) {
231 feed(data);
232 feed(null);
233 }
234
235 /++
236 Feed it some data you have ready.
237
238 Feed it an empty array or `null` to indicate end of input.
239 +/
240 void feed(in char[] data) {
241 feed(cast(const(ubyte)[]) data);
242 }
243
244 /// ditto
245 void feed(in ubyte[] data) {
246 const(ubyte)[] toProcess;
247 if(unprocessedData.length) {
248 unprocessedData ~= data;
249 toProcess = unprocessedData;
250 } else {
251 toProcess = data;
252 }
253
254 auto eol = toProcess.indexOf("\n");
255 if(eol == -1) {
256 unprocessedData = cast(ubyte[]) toProcess;
257 } else {
258 // if it is \r\n, remove the \r FIXME
259 // if it is \r\n<space>, need to concat
260 // if it is \r\n\t, also need to concat
261 processLine(toProcess[0 .. eol]);
262 }
263 }
264
265 /// ditto
266 void feed(typeof(null)) {
267 feed(cast(const(ubyte)[]) null);
268 }
269
270 private ubyte[] unprocessedData;
271
272 private void processLine(in ubyte[] line) {
273
274 }
275 }
276
277 immutable monthNames = [
278 "",
279 "January",
280 "February",
281 "March",
282 "April",
283 "May",
284 "June",
285 "July",
286 "August",
287 "September",
288 "October",
289 "November",
290 "December"
291 ];