thombruce / toodles

✅ A super simple todo app
https://toodles.thombruce.com/
GNU General Public License v3.0
0 stars 0 forks source link

[Feature]: Repeatable todos #118

Closed thombruce closed 2 months ago

thombruce commented 2 months ago

Feature request

If a todo has an every: tag with a valid tag value, then when it is marked as done it should create a clone with a new due date. If no due date is present, then the new due date could be derived from... created date? Maybe. Certainly the value of every: will come into play; every:Tuesday for instance should look for the next Tuesday.

Imagine the scenario that someone has created a todo on a Monday, Put bins out every:Tuesday, and they have marked it done on Monday evening - the "next" Tuesday is the very next day. Contextually, we understand that isn't the intended Tuesday in this case, but Toodles... Toodles shouldn't. If they have created that todo on a Monday and marked it done on that Monday, even if it says every:Tuesday we should still create the new todo for the very next day. I know that's wrong 99% of the time, and it isn't user error - it's our error - but Toodles shouldn't try to intuit the correct date; it should just pick the next valid date, which may well be the very next day and not the intended one.

If that same todo were marked done on a Tuesday, it should select the next Tuesday. Simple. Correct.

In the case where our automatically chosen date is incorrect, the user should be able to modify the due date.

This is what the due date exists for and is useful for, in terms of repetition via our every: tag.

If that same todo had a due date, and that due date were that Tuesday (the very next day from creation), Toodles will know then that the intended next day is 8 days in the future (one week from the previous due date).

So no intuition, just straightforward cloning based on given information.

I feel like this is going to be simple when implemented, but there's potential for some trickiness.

Code of Conduct

thombruce commented 2 months ago

Giving this some thought... because we could write our own simple and generic solution. It accepts a few string patterns and tries to match those to a rule, and this rule is used to determine the new date. Straightforward... -ish, but it could be a big chunk of switch/case and if/else statements as we narrow down the string: Is it a weekday? Is it a day of the month? Is it on a monthly or a bi-monthly interval? And when we've determined those rules, we need to convert them into a dayjs date object by way of saying... I dunno... find me the second Tuesday of the month after next? It's going to get complicated.

Now, I don't mind writing very simple aliases for semi-complex rules, but when it does come to increased complexity I think we do need to lean on an existing syntax.

For that, there's...

My solution?

Probably to try and mould the rrule JS library until it feels a bit like ice_cube; we only need to invoke the subset of rules we explicitly support anyway (some may not fit in the strict syntax of todo.txt) so... seems like the right approach to me.

With our own limited ruleset being based on an rrule backed API, we can also always expand it to permit more of the syntax from that iCalendar RFC... as and when.

thombruce commented 2 months ago

Although hey, look at this:

// Get an iCalendar RRULE string representation:
// The output can be used with RRule.fromString().
rule.toString()
"DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR"

Ignore DTSTART:20120201T093000Z\n, as we can get the date to start from our created date.

Also ignore RRULE: which is just the key describing where the rule itself actually begins (presence of colon will break our todo spec).

What we're left with is technically a valid tag value:

FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR

No spaces, no colons - those are our only two rules right now. It does contain semicolons and commas - I'd rather it didn't as I would like users to be able to use punctuation against tags but... since these aren't followed by a space we can tweak our matching to account for that.

I don't want to have to write a string like that whenever I want simple recursion but... actually supporting it might be very straightforward. We simply parse that syntax using rrule if a previous case hasn't been matched.

So we could come at this backwards... support the advanced format first and then reduce necessary complexity by creating custom patterns.


None of this answers the question of where/how we handle the cloning on toggleDone() yet. Really what we want to do is something like new Todo(oldTodo) or... Todo.clone(oldTodo)... and... set a new due date according to the value of the every: tag. So it'll be something like...

if (todo.shouldRepeat) { // where shouldRepeat is checking for the presence of `every:`
  newTodo = Todo.clone(todo) // or todo.clone()
}
todo.toggleDone()
}

Now... alternative to that, because I'm thinking the Class actually doesn't have any sense that it is working in tandem with our store - it doesn't know that each of its instances is part of a collection, rather than clone() we might say...

newTodo = todo.next()

If the todo next() is being called on should not repeat, then that will return a falsy value (probably undefined). Otherwise... it yields a brand new Todo instance which resembles the first in every way but its created and due dates.

That seems syntactically a lot nicer than saying clone() where we have no idea of the purpose of the clone and we... really shouldn't infer that the purpose is to get the next value. next() makes sense.

So we would, on closing a todo, toggleDone() AND get next().

The problem is, and I think this is what's been slightly tripping me up, toggleDone() toggles the todo both ways... and we only want this to happen on closing. We need to be calling close() instead, which means some logic needs to migrate out of the model and into the store.