recurrences.mjs 5.44 KB
Newer Older
1
"use strict";
2
import { userData, getActiveFile } from "../render.js";
3
import { items } from "./todos.mjs";
4
import { convertDate } from "./date.mjs";
5
6
7
8

function splitRecurrence(recurrence) {
  let mul = 1;
  let period = recurrence;
9
  let plus = false;
10
  if(recurrence !== undefined && recurrence.length > 1) {
11
12
13
14
15
16
17
18
19
20
    if (recurrence.substr(0,1) == "+") {
      plus = true;
      if (recurrence.length > 2)
        mul = Number(recurrence.substr(1, recurrence.length - 2));
    } else {
      mul = Number(recurrence.substr(0, recurrence.length - 1));
    }
    if (mul == 0) {
      mul = 1;
    }
21
22
23
24
25
    period = recurrence.substr(-1);
  }
  return {
    mul: mul,
    period: period,
26
    plus: plus,
27
28
    toString: function() {
      return this.mul == 1 || this.period === undefined ?
29
        this.period : (this.plus ? "+" : "") + this.mul + this.period;
30
31
32
33
34
35
36
    }
  };
}
function generateRecurrence(todo) {
  try {
    // duplicate not reference
    let recurringTodo = Object.assign({}, todo);
37
38
39
    recurringTodo.date = new Date;

    // if the item to be duplicated has been completed before the due date, the recurring item needs to be set incomplete again
40
41
    recurringTodo.complete = false;
    recurringTodo.completed = null;
42

43
    // adjust due and threshold dates
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    let recSplit = splitRecurrence(todo.rec);
    if (recSplit.plus) {
      // strict recurrence is based on previous date value
      if (todo.t)
        recurringTodo.t = addIntervalToDate(todo.t, recSplit.mul, recSplit.period);
      if (todo.due)
        recurringTodo.due = addIntervalToDate(todo.due, recSplit.mul, recSplit.period);
    } else {
      // non-strict recurrence is based on today's date
      if (todo.t) {
        let threshold_base = new Date();
        if (todo.due) {
          // preserve interval between threshold and due date
          let interval = todo.due - todo.t;  // millisec
          threshold_base = new Date(threshold_base.getTime() - interval);
        }
        recurringTodo.t = addIntervalToDate(threshold_base, recSplit.mul, recSplit.period);
        console.log("t based on today plus rec: " + recurringTodo + " todo.t is " + recurringTodo.t);
      }
      if (todo.due)
        recurringTodo.due = addIntervalToDate(new Date(), recSplit.mul, recSplit.period);
    }
    if (!todo.t && !todo.due) {
      // This is an ambiguous case: there is a rec: tag but no dates to apply it to.
      // Some would prefer to make this a no-op, but past versions of sleek have added
      // a due date when there is only a recurrence, so we will continue to do that here.
      recurringTodo.due = addIntervalToDate(new Date(), recSplit.mul, recSplit.period);
    }
    // the following strings are magic from jsTodoTxt and must be changed when dates are changed
    // because TodoTxtItem.toString() relies on the fieldString values, not the field values themselves.
    if (recurringTodo.t)
      recurringTodo.tString = convertDate(recurringTodo.t);
    if (recurringTodo.due)
      recurringTodo.dueString = convertDate(recurringTodo.due);

79
80
81
82
83
    // get index of recurring todo
    const index = items.objects.map(function(item) {return item.toString().replaceAll(String.fromCharCode(16)," "); }).indexOf(recurringTodo.toString().replaceAll(String.fromCharCode(16)," "));
    // only add recurring todo if it is not already in the list
    if(index===-1) {
      items.objects.push(recurringTodo);
84
85
      window.api.send("writeToFile", [items.objects.join("\n").toString() + "\n"]);
      return Promise.resolve("Success: Recurring todo created and written into file: " + getActiveFile());
86
87
88
89
90
91
92
93
    } else {
      return Promise.resolve("Info: Recurring todo already in file, won't write anything");
    }
  } catch(error) {
    error.functionName = generateRecurrence.name;
    return Promise.reject(error);
  }
}
94
95
96
97
98

// addIntervalToDate used to compute recurrences, but now also used to add
// or subtract a time interval to/from dates in filter query language.
// Therefore it must now handle negative count values.
function addIntervalToDate(due, count, unit) {
99
  let days = 0;
100
  let months = 0;
101
  switch (unit) {
102
103
104
    case "b":
      // add "mul" business days, defined as not Sat or Sun
      {
105
106
        let incr = (count >= 0)? 1: -1;
        let bdays_left = count * incr;
107
108
109
        let millisec_due = due.getTime();
        let day_of_week = due.getDay(); // 0=Sunday, 1..5 weekday, 6=Saturday
        while (bdays_left > 0) {
110
111
112
113
114
115
116
          millisec_due += 1000 * 60 * 60 * 24 * incr;  // add a day to time
          if (incr > 0) {
            day_of_week = (day_of_week < 6 ? day_of_week + incr : 0);
          } else {
            day_of_week = (day_of_week > 0 ? day_of_week + incr : 6);
          }
          if (day_of_week > 0 && day_of_week < 6) {
117
118
119
120
121
            bdays_left--;  // one business day step accounted for!
          }
        }
        return new Date(millisec_due);
      }
122
123
124
125
126
127
128
    case "d":
      days = 1;
      break;
    case "w":
      days = 7;
      break;
    case "m":
129
      months = 1;
130
131
      break;
    case "y":
132
      months = 12;
133
134
      break;
  }
135
  if (months > 0) {
136
    let due_month = due.getMonth() + count * months;
137
138
139
140
141
142
    let due_year = due.getFullYear() + Math.floor(due_month/12);
    due_month = due_month % 12;
    let monthlen = new Date(due_year, due_month+1, 0).getDate();
    let due_day = Math.min(due.getDate(), monthlen);
    return new Date(due_year, due_month, due_day);
  }
143
  due = due.getTime();
144
  due += 1000 * 60 * 60 * 24 * count * days;
145
146
147
  return new Date(due);
}

148
export { splitRecurrence, generateRecurrence, addIntervalToDate };