Fix stringsource parsing to that apostrophes don't count as quotes.
[schedulator.git] / stringsource.cs
blobbba332e83f17a96d16be75f4b84efbd9bdf759a5
1 using System;
2 using System.IO;
3 using System.Collections;
4 using System.Text.RegularExpressions;
5 using Wv;
6 using Wv.Schedulator;
8 namespace Wv.Schedulator
10 public class StringSource : Source
12 protected string[] lines;
13 WvLog log;
15 public StringSource(Schedulator s, string name, string[] lines)
16 : base(s, name)
18 log = new WvLog(name);
19 this.lines = lines;
21 // process "import" lines right away, to create additional
22 // sources.
23 foreach (string line in lines)
25 string[] args = word_split(line.Trim());
26 string cmd = wv.shift(ref args).ToLower();
28 if (cmd == "import" || cmd == "plugin")
30 log.print("Creating plugin from line: '{0}'\n", line);
31 if (args.Length < 2)
32 err(0, "Not enough parameters to '{0}'", cmd);
33 else
35 SourceRegistry reg = new SourceRegistry();
36 reg.create(s, dequote(args[0]), dequote(args[1]));
42 public static Source create(Schedulator s, string name,
43 string prefix, string suffix)
45 return new StringSource(s, name, suffix.Split('\n'));
48 static string[] get_file(string filename)
50 try
52 StreamReader r = File.OpenText(filename);
53 return r.ReadToEnd().Split('\n');
55 catch (IOException)
57 // nothing
59 return "".Split('\n');
62 public static Source create_from_file(Schedulator s, string name,
63 string prefix, string suffix)
65 return new StringSource(s, name, get_file(suffix));
68 public static string[] word_split(string s)
70 string bra = "\"'([{";
71 string ket = "\"')]}";
73 ArrayList list = new ArrayList();
74 Stack nest = new Stack();
75 string buf = ""; // even if it's empty, always add the first word
76 bool last_was_white = true;
77 foreach (char c in s)
79 int is_bra = bra.IndexOf(c);
80 int is_ket = ket.IndexOf(c);
82 if (c == '\'' && !last_was_white)
83 is_bra = -1;
85 if (nest.Count == 0)
87 if (c == ' ' || c == '\t')
89 if (buf != null)
90 list.Add(buf);
91 buf = null;
93 else
94 buf += c;
96 else
98 buf += c;
100 if (is_ket >= 0 && (char)nest.Peek() == c)
102 nest.Pop();
103 last_was_white = true;
104 continue;
108 if (is_bra >= 0)
109 nest.Push(ket[is_bra]);
111 last_was_white = (c == ' ' || c == '\t');
114 // even if it's empty, always add the last word
115 list.Add(buf==null ? "" : buf);
117 string[] result = new string[list.Count];
118 int ii = 0;
119 foreach (string ss in list)
120 result[ii++] = ss;
121 return result;
124 void err(int lineno, string fmt, params object[] args)
126 log.print(lineno.ToString() + ": " + fmt, args);
129 static string dequote(string s, string bra, string ket)
131 int idx_bra = bra.IndexOf(s[0]);
132 int idx_ket = ket.IndexOf(s[s.Length-1]);
134 if (idx_bra != -1 && idx_bra == idx_ket)
135 return s.Substring(1, s.Length-2);
136 else
137 return s;
140 static string dequote(string s, string bra)
142 return dequote(s, bra, bra);
145 static string dequote(string s)
147 return dequote(s, "\"'");
150 public static TimeSpan parse_estimate(int lineno, string s)
152 Regex re = new Regex(@"([0-9]*(\.[0-9]*)?)\s*([a-zA-Z]*)");
153 Match match = re.Match(s);
154 GroupCollection grp = match.Groups;
155 if (!match.Success || grp.Count < 4)
157 //err(lineno, "Can't parse estimate '{0}'", s);
158 return TimeSpan.FromHours(0);
161 double num = wv.atod(grp[1].ToString());
162 // wv.printerr("parsing time '{0}' as '{1}'\n", grp[1].ToString(), num);
163 string units = grp[3].ToString().ToLower() + " ";
164 switch (units[0])
166 case 'd':
167 return TimeSpan.FromHours(num*8);
168 case 'h':
169 case ' ':
170 return TimeSpan.FromHours(num);
171 case 'm':
172 return TimeSpan.FromMinutes(num);
173 case 's':
174 return TimeSpan.FromSeconds(num); // wow, you're fast!
177 //err(lineno, "Unknown unit '{0}' in '{1}'", units, s);
178 return TimeSpan.FromHours(0);
181 class DelayedTask
183 public Source source;
184 public bool external;
185 public Task oldtask, task;
186 public Task parent;
187 public DelayedTask dtparent;
189 public int lineno;
190 public string name;
192 public FixFor fixfor;
193 public int priority;
195 public DateTime donedate, startdate, duedate;
197 public TimeSpan currest = TimeSpan.MaxValue;
198 public TimeSpan elapsed = TimeSpan.MaxValue;
199 public bool done;
200 public DateSlider habits;
202 public string make_id()
204 // this needs to be "as unique as possible" given that
205 // all these tasks come from a text file, and yet not
206 // change when the text file changes. It's impossible to
207 // be perfect here, so we do what we can.
208 if (dtparent != null)
209 return dtparent.make_id() + ":" + name;
210 else
211 return name;
214 public void apply_from(Task t)
216 if (fixfor == null)
217 fixfor = t.fixfor;
218 if (priority <= 0)
219 priority = t.priority;
221 if (wv.isempty(donedate))
222 donedate = t.donedate;
223 if (wv.isempty(startdate))
224 startdate = t.startdate;
225 if (wv.isempty(duedate))
226 duedate = t.duedate;
227 if (currest == TimeSpan.MaxValue)
228 currest = t.currest;
229 if (elapsed == TimeSpan.MaxValue)
230 elapsed = t.elapsed;
231 if (!done)
232 done = t.done;
233 if (habits == null)
234 habits = t.habits;
237 public void apply_to(Task t)
239 task = t;
240 if (dtparent != null && dtparent.task != null)
241 parent = dtparent.task;
242 if (parent != null)
243 t.parent = parent;
244 if (t.parent == t)
245 throw new ArgumentException("task is its own parent!");
247 if (fixfor != null)
248 t.fixfor = fixfor;
249 if (priority > 0)
250 t.priority = priority;
252 if (!wv.isempty(donedate))
253 t.donedate = donedate;
254 else if (wv.isempty(t.donedate))
255 t.donedate = source.s.align;
256 if (!wv.isempty(startdate))
257 t.startdate = startdate;
258 if (!wv.isempty(duedate))
259 t.duedate = duedate;
260 if (currest != TimeSpan.MaxValue && wv.isempty(t.currest))
261 t.currest = currest;
262 if (elapsed != TimeSpan.MaxValue && wv.isempty(t.elapsed))
263 t.elapsed = elapsed;
264 if (done)
265 t.done = done;
266 if (habits != null)
267 t.habits = habits;
270 public class IdCompare : IComparer
272 public int Compare(object _x, object _y)
274 DelayedTask x = (DelayedTask)_x;
275 DelayedTask y = (DelayedTask)_y;
277 return x.lineno.CompareTo(y.lineno);
281 // this is a simple ordering that makes parents come out
282 // before their children, so that we can be sure to fill in
283 // the parent's values before we fill in its children's values
284 // by copying the parent.
285 public class TopologicalCompare : IComparer
287 public int Compare(object _x, object _y)
289 DelayedTask x = (DelayedTask)_x;
290 DelayedTask y = (DelayedTask)_y;
292 if (x.dtparent == y.dtparent)
293 return 0; // effectively equal, even if null
294 else if (x.dtparent == null)
295 return Compare(x, y.dtparent);
296 else if (y.dtparent == null)
297 return Compare(x.dtparent, y);
298 else if (x.dtparent != y.dtparent)
299 return Compare(x.dtparent, y.dtparent);
300 else
301 return x.lineno.CompareTo(y.lineno);
306 class DtIndex
308 public DelayedTask parent;
309 public string name;
311 public DtIndex(DelayedTask parent, string name)
313 this.parent = parent;
314 this.name = name;
317 public override bool Equals(object o)
319 DtIndex y = o as DtIndex;
320 if (y == null)
321 return false;
322 else
323 return parent == y.parent && name == y.name;
326 public override int GetHashCode()
328 return name.GetHashCode();
332 ArrayList dtasks = new ArrayList();
333 Hashtable created_tasks = new Hashtable();
335 Task find_task(string title)
337 foreach (Task t in s.tasks)
338 if (t.name == title || t.moniker == title)
339 return t;
340 return null;
343 void parse_task(int level, string[] args,
344 ArrayList parents, int lineno,
345 ref FixFor last_fixfor, DateSlider habits,
346 bool external)
348 if (level > parents.Count)
349 err(lineno, "Level-{0} bug with only {1} parents!",
350 level, parents.Count);
351 else if (level <= parents.Count)
352 parents.RemoveRange(level, parents.Count - level);
354 int pri = -1;
355 DateTime startdate = DateTime.MinValue;
356 DateTime duedate = DateTime.MinValue;
357 DateTime donedate = DateTime.MinValue;
358 TimeSpan currest = TimeSpan.MaxValue;
359 TimeSpan elapsed = TimeSpan.MaxValue;
360 for (int i = 0; i < args.Length; i++)
362 string a = args[i];
363 if (a[0] == '[' && a[a.Length-1] == ']')
365 a = dequote(wv.shift(ref args, i), "[", "]");
366 --i; // we just ate this array element!
368 string[] words = word_split(a);
369 for (int wi = 0; wi < words.Length; wi++)
371 string word = words[wi].ToLower();
372 if (word == "start" && wi+1 < words.Length)
374 startdate = wv.date(words[wi+1]).Date;
375 wi++;
377 else if ((word == "end" || word == "due")
378 && wi+1 < words.Length)
380 duedate = wv.date(words[wi+1]).Date;
381 wi++;
383 else if ((word == "done" || word == "finished")
384 && wi+1 < words.Length)
386 donedate = wv.date(words[wi+1]).Date;
387 wi++;
389 else if (word[0] == 'p')
390 pri = Int32.Parse(word.Substring(1));
391 else if (Char.IsDigit(word[0]) || word[0] == '.')
393 if (currest == TimeSpan.MaxValue)
394 currest = parse_estimate(lineno, word);
395 else if (elapsed == TimeSpan.MaxValue)
396 elapsed = parse_estimate(lineno, word);
397 else
398 err(lineno, "Extra time '{0}'", word);
400 else
401 err(lineno, "Unknown flag '{0}' in '{1}'\n",
402 word, a);
407 DelayedTask parent = (parents.Count > 0
408 ? (DelayedTask)parents[parents.Count-1]
409 : null);
410 string title = String.Join(" ", args);
412 DelayedTask d = new DelayedTask();
413 d.source = this;
414 d.lineno = lineno;
415 d.name = title;
416 d.external = external;
418 d.dtparent = parent;
419 d.fixfor = last_fixfor;
420 d.priority = pri;
421 d.donedate = donedate;
422 d.startdate = startdate;
423 d.duedate = duedate;
424 d.currest = currest;
425 d.elapsed = elapsed;
426 if (elapsed != TimeSpan.MaxValue && currest == elapsed)
427 d.done = true;
428 d.habits = habits;
430 //log.print("Parent of '{0}' is '{1}'\n",
431 // d.name, d.dtparent==null ? "(none)" : d.dtparent.name);
433 parents.Add(d);
434 dtasks.Add(d);
437 public override void make_basic()
439 int lineno = 0;
440 ArrayList parents = new ArrayList();
441 string projname = "";
442 FixFor last_fixfor = null;
443 DateSlider habits = null;
445 foreach (string str in lines)
447 string[] args = word_split(str.Trim());
448 string cmd = wv.shift(ref args).ToLower();
450 ++lineno;
452 if (cmd == "" || cmd[0] == '#') // blank or comment
453 continue;
455 switch (cmd)
457 case "import":
458 case "plugin":
459 // already handled earlier
460 break;
462 case "milestone":
463 case "release":
464 case "version":
465 if (args.Length > 0)
467 string fixforname = dequote(args[0]);
468 int idx = fixforname.IndexOf(':');
469 if (idx >= 0)
471 projname = fixforname.Substring(0, idx);
472 fixforname = fixforname.Substring(idx+1);
474 last_fixfor =
475 s.fixfors.Add(s.projects.Add(projname),
476 fixforname);
478 else
479 err(lineno, "'{0}' requires an argument", cmd);
480 if (args.Length > 1)
481 last_fixfor.add_release(wv.date(args[1]));
483 log.print("New milestone: {0}\n", last_fixfor.name);
484 break;
486 case "bounce":
487 if (last_fixfor != null)
488 foreach (string arg in args)
489 last_fixfor.add_release(wv.date(arg));
490 else
491 err(lineno,
492 "Can't 'bounce' until we have a 'milestone'");
493 break;
495 case "loadfactor":
496 if (habits == null)
497 habits = s.default_habits;
498 habits = habits.new_loadfactor(wv.atod(args[0]));
499 break;
501 case "workinghours":
502 if (habits == null)
503 habits = s.default_habits;
504 if (args.Length < 7)
505 err(lineno,
506 "'Workinghours' needs exactly 7 numbers");
507 else
509 double[] hpd = new double[7];
510 for (int i = 0; i < 7; i++)
511 hpd[i] = wv.atod(args[i]);
512 habits = habits.new_hours_per_day(hpd);
514 break;
516 case "alignday":
517 s.align = wv.date(args[0]);
518 break;
520 case "today":
521 s.now = wv.date(args[0]);
522 break;
524 case "*":
525 case "**":
526 case "***":
527 case "****":
528 case "*****":
529 case "******":
530 case "*******":
531 case "********":
532 case "*********":
533 case "**********":
534 parse_task(cmd.Length-1, args,
535 parents, lineno, ref last_fixfor, habits,
536 false);
537 break;
539 case "!":
540 parse_task(parents.Count, args,
541 parents, lineno, ref last_fixfor, habits,
542 true);
543 break;
545 default:
546 err(lineno, "Unknown command '{0}'!", cmd);
547 break;
550 if (habits != null)
552 s.default_habits = habits;
553 if (last_fixfor != null)
555 last_fixfor.default_habits = habits;
556 last_fixfor.project.default_habits = habits;
562 public override Task[] make_tasks()
564 // go back to the original order before adding the tasks,
565 // so the list looks the way the user wanted it
566 dtasks.Sort(new DelayedTask.IdCompare());
568 foreach (DelayedTask d in dtasks)
570 if (!d.external)
572 DtIndex idx = new DtIndex(d.dtparent, d.name);
574 // prevent creation of duplicate tasks: simply modify
575 // the existing task instead
576 d.task = (Task)created_tasks[idx];
577 if (d.task == null)
579 d.task = s.tasks.Add(this, d.make_id(), d.name);
580 created_tasks.Add(idx, d.task);
585 return null;
588 public override void cleanup_tasks()
590 foreach (DelayedTask d in dtasks)
592 if (d.external)
594 d.task = find_task(d.name);
595 if (d.task != null)
596 d.apply_from(d.task);
598 if (d.task == null)
599 d.task = s.tasks.Add(this, d.make_id(), d.name);
602 // let's fill any optional information from parents into
603 // children. First, we'll want to sort so that parents
604 // always come before children, so we're guaranteed to have
605 // finished any given parent before we get to its child.
606 dtasks.Sort(new DelayedTask.TopologicalCompare());
608 foreach (DelayedTask d in dtasks)
610 DelayedTask p = d.dtparent;
611 if (p != null)
613 if (d.fixfor == null)
614 d.fixfor = p.fixfor;
615 if (d.priority < 0)
616 d.priority = p.priority;
617 if (d.habits == null)
618 d.habits = p.habits;
620 d.apply_to(d.task);