diff --git a/calendar.d b/calendar.d new file mode 100644 index 0000000..43fef57 --- /dev/null +++ b/calendar.d @@ -0,0 +1,275 @@ +/++ + + OpenD could use automatic mixin to child class... + + Extensions: color. exrule? trash day - if holiday occurred that week, move it forward a day + + Standards: categories + + UI idea for rrule: show a mini two year block with the day highlighted + -> also just let user click on a bunch of days so they can make a list + + Want ability to add special info to a single item of a recurring event + + Can use inotify to reload ui when sqlite db changes (or a trigger on postgres?) + + https://datatracker.ietf.org/doc/html/rfc5545 + https://icalendar.org/ ++/ +module arsd.calendar; + +import arsd.core; + +import std.datetime; + +/++ + History: + Added July 3, 2024 ++/ +SimplifiedUtcTimestamp parseTimestampString(string when, SysTime relativeTo) /*pure*/ { + import std.string; + + int parsingWhat; + int bufferedNumber = int.max; + + int secondsCount; + + void addSeconds(string word, int bufferedNumber, int multiplier) { + if(parsingWhat == 0) + parsingWhat = 1; + if(parsingWhat != 1) + throw ArsdException!"unusable timestamp string"("you said 'at' but gave a relative time", when); + if(bufferedNumber == int.max) + throw ArsdException!"unusable timestamp string"("no number before unit", when, word); + secondsCount += bufferedNumber * multiplier; + bufferedNumber = int.max; + } + + foreach(word; when.split(" ")) { + word = strip(word).toLower().replace(",", ""); + if(word == "in") + parsingWhat = 1; + else if(word == "at") + parsingWhat = 2; + else if(word == "and") { + // intentionally blank + } else if(word.indexOf(":") != -1) { + if(secondsCount != 0) + throw ArsdException!"unusable timestamp string"("cannot mix time styles", when, word); + + if(parsingWhat == 0) + parsingWhat = 2; // assume absolute time when this comes in + + bool wasPm; + + if(word.length > 2 && word[$-2 .. $] == "pm") { + word = word[0 .. $-2]; + wasPm = true; + } else if(word.length > 2 && word[$-2 .. $] == "am") { + word = word[0 .. $-2]; + } + + // FIXME: what about midnight? + int multiplier = 3600; + foreach(part; word.split(":")) { + import std.conv; + secondsCount += multiplier * to!int(part); + multiplier /= 60; + } + + if(wasPm) + secondsCount += 12 * 3600; + } else if(word.isNumeric()) { + import std.conv; + bufferedNumber = to!int(word); + } else if(word == "seconds" || word == "second") { + addSeconds(word, bufferedNumber, 1); + } else if(word == "minutes" || word == "minute") { + addSeconds(word, bufferedNumber, 60); + } else if(word == "hours" || word == "hour") { + addSeconds(word, bufferedNumber, 60 * 60); + } else + throw ArsdException!"unusable timestamp string"("i dont know what this word means", when, word); + } + + if(parsingWhat == 0) + throw ArsdException!"unusable timestamp string"("couldn't figure out what to do with this input", when); + + else if(parsingWhat == 1) // relative time + return SimplifiedUtcTimestamp((relativeTo + seconds(secondsCount)).stdTime); + else if(parsingWhat == 2) { // absolute time (assuming it is today in our time zone) + auto today = relativeTo; + today.hour = 0; + today.minute = 0; + today.second = 0; + return SimplifiedUtcTimestamp((today + seconds(secondsCount)).stdTime); + } else + assert(0); +} + +unittest { + auto testTime = SysTime(DateTime(Date(2024, 07, 03), TimeOfDay(10, 0, 0)), UTC()); + void test(string what, string expected) { + auto result = parseTimestampString(what, testTime).toString; + assert(result == expected, result); + } + + test("in 5 minutes", "2024-07-03T10:05:00Z"); + test("in 5 minutes and 5 seconds", "2024-07-03T10:05:05Z"); + test("in 5 minutes, 45 seconds", "2024-07-03T10:05:45Z"); + test("at 5:44", "2024-07-03T05:44:00Z"); + test("at 5:44pm", "2024-07-03T17:44:00Z"); +} + +version(none) +void main() { + auto e = new CalendarEvent( + start: DateTime(2024, 4, 22), + end: Date(2024, 04, 22), + ); +} + +class Calendar { + CalendarEvent[] events; +} + +/++ + ++/ +class CalendarEvent { + DateWithOptionalTime start; + DateWithOptionalTime end; + + Recurrence recurrence; + + int color; + string title; // summary + string details; + + string uid; + + this(DateWithOptionalTime start, DateWithOptionalTime end, Recurrence recurrence = Recurrence.none) { + this.start = start; + this.end = end; + this.recurrence = recurrence; + } +} + +/++ + ++/ +struct DateWithOptionalTime { + string tzlocation; + DateTime dt; + bool hadTime; + + @implicit + this(DateTime dt) { + this.dt = dt; + this.hadTime = true; + } + + @implicit + this(Date d) { + this.dt = DateTime(d, TimeOfDay.init); + this.hadTime = false; + } + + this(in char[] s) { + // FIXME + } +} + +/++ + ++/ +struct Recurrence { + static Recurrence none() { + return Recurrence.init; + } +} + +/+ + +enum FREQ { + +} + +struct RRULE { + FREQ freq; + int interval; + int count; + DAY wkst; + + // these can be negative too indicating the xth from the last... + DAYSET byday; // ubyte bitmask... except it can also have numbers atached wtf + + // so like `BYDAY=-2MO` means second-to-last monday + + MONTHDAYSET byMonthDay; // uint bitmask + HOURSET byHour; // uint bitmask + MONTHDSET byMonth; // ushort bitmask + + WEEKSET byWeekNo; // ulong bitmask + + int BYSETPOS; +} + ++/ + +struct ICalParser { + // if the following line starts with whitespace, remove the cr/lf/ and that ONE ws char, then add to the previous line + // it is supposed to support this even if it is in the middle of a utf-8 sequence + // contentline = name *(";" param ) ":" value CRLF + // you're supposed to split lines longer than 75 octets when generating. + + void feedEntireFile(in ubyte[] data) { + feed(data); + feed(null); + } + void feedEntireFile(in char[] data) { + feed(data); + feed(null); + } + + /++ + Feed it some data you have ready. + + Feed it an empty array or `null` to indicate end of input. + +/ + void feed(in char[] data) { + feed(cast(const(ubyte)[]) data); + } + + /// ditto + void feed(in ubyte[] data) { + const(ubyte)[] toProcess; + if(unprocessedData.length) { + unprocessedData ~= data; + toProcess = unprocessedData; + } else { + toProcess = data; + } + + auto eol = toProcess.indexOf("\n"); + if(eol == -1) { + unprocessedData = cast(ubyte[]) toProcess; + } else { + // if it is \r\n, remove the \r FIXME + // if it is \r\n, need to concat + // if it is \r\n\t, also need to concat + processLine(toProcess[0 .. eol]); + } + } + + /// ditto + void feed(typeof(null)) { + feed(cast(const(ubyte)[]) null); + } + + private ubyte[] unprocessedData; + + private void processLine(in ubyte[] line) { + + } +}