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