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 ];