Ambiguity in the days operator?

I am monitoring my first attempt at a plugin which uses the days operator, it’s all about reminding a user to review a tiddler if it is considered overdue a review.

So if a tiddler is set to be reviewed every 30 days then …

if 31 days have elapsed since last review it should display a red bell icon in the tiddler toolbar but if 30 days or less have elapsed it should show a green icon.

it would work just as well if the logic was…

if 30 days have elapsed since last review it should display a red bell icon in the toolbar but if 29 days or less have elapsed it should show a green icon.

So I am not overly concerned what happens on the ‘anniversary day’ as long as the decision is mutually exclusive - on that day I don’t want the logic to say that the tiddler is both overdue a review but also not overdue a review.

The days documentation is unclear on this fine point.

The examples does seem to offer non-ambiguity if they were considered function spec.

https://tiddlywiki.com/static/days%20Operator%20%28Examples%29.html

[!days:created[-800]]
→ tiddlers created more than 800 days ago

The word ‘more’ suggests that tiddlers created 800 days ago will not be included it is necessary to go to 801 days to see the change in the output - the days algorithm converts the UTC time code to days by ignoring the time part of the date.

‘more’ is obviously unambiguous with integers, 4 is more than 3, 3 is not more than 3 - we are dealing with whole days with the number of hours minutes and seconds past midnight (UTC) being ignored.

[days[-14]]
→ tiddlers modified within the last 14 days

This sounds a little more ambiguous when dealing with intervals does -3 lie within the interval [-3, 0].

I think I can see the code responsible for the current behaviour (see) $:/core/modules/filters/days.js

		isWithinDays = function(dateField) {
			var sign = $tw.utils.sign(targetTimeStamp - (new Date(dateField)).setHours(0,0,0,0));
			return **sign === 0** || sign === dayIntervalSign;
		};

The sign === 0 clause leads to the ambiguity.

The function isWithinDays is internal to the days operator and is used for both clauses, one handling the…

if(operator.prefix === "!")

case and the other handling the no-prefix case but they both use the isWithinDays function.

This means that the documented example…

[!days:created[-800]]
→ tiddlers created more than 800 days ago

Would have to be re written as follows to be accurate…

[!days:created[-800]]
→ tiddlers created 800 days ago OR more than 800 days ago

So for instance if I created a tiddler yesterday then

[!days:created[-1]]

And

[days:created[-1]]

Will both be true today but tomorrow only the first will be true and yesterday only the second was be true but for today…they are both true. For the purpose of my code the ambiguity in the result experienced on one day - ‘today’ is undesirable, it actually results in two icons appearing in the tiddler toolbar for that single day where my intention is to have only one.

This is because my code does not use conditionals, instead I have two filter expressions, they are in the same file, they appear one after the other but are independent but intended to have mutually exclusive behaviour, the desired IF … ELSE … conditional is implied by assuming this is already taken care of in the days operator.

The suggestion would be if the documented examples for days listed above are considered function spec then two functions are required…

isWithinDays() as at present

isMoreDays() to support the “prefix !” case - this function would not include the sign === 0 condition in it’s return statement.

I suggest that mutual exclusivity between the “prefix!” case and the “non-prefix!” case is important for the usefulness of the days operator - coders can compensate for “boundary conditions” when comparing days with integer intervals that represent days but mutual exclusivity between “outside” and “within” is essential - to say something is both outside and within could lead to all kinds of issues - someone writing code to decide whether to send a text message fining someone for late return of an item or alternatively to send a message telling them to renew today to avoid a fine tomorrow would probably prefer only one of these messages to be sent today :crazy_face:

Without looking too deeply, I presume todays date/time date is made UTC before removing the time?, because the day can change because of the time now? I live in the +10 time zone, so before 10am will be a different day in UTC.

I found when battling the use of the days operator that more often than not I needed one days operator to follow the other to define a specific period in time that does not extend to the past or future.

  • This can be reworded
    → tiddlers created 800 days or more ago
  • and is perhaps the intention.

I will think about this a little more…

Hi TW_Tones - sure the example could be reworded to resolve disagreement between “spec” and code behaviour but I think non-exclusivity on anniversary dates degrades the usefulness of the days operator considerably - take the example I appended to my first post regards whether to issue a late return fine or issue a message saying “last chance!!! renew today to avoid a fine”.

Date logic tends to lead to binary decisions, gym membership has either expired or not - it’s not useful to have a ‘grey day’ you cannot simultaneously be allowed to use the gym and not allowed to use the gym.

Also if a period of ‘grey’ was required so that you can use the gym after your membership has elapsed then the coder using the days operator might want to have a grey period of 2 days or 3, at the moment the grey period is not an option but hard coded as one day.

I am making a case that the ambiguity might only rarely be fortuitously beneficial but will often be non-beneficial.

This may be true, But I have not yet seen it for myself. But consider that you expectation of what the negation ! means may be poorly documented. See the days operator where no negations says " D days in the…" and the negation “is at least D days in the…”. So I think its behaving as advertised.

[days: created[-800]] would read; ?

Those tiddlers who’s created date is within 800 days in the past.

[!days: created[-800]] would read;

Those tiddlers who’s created date is at least 800 days in the past.

Not withstanding this the documentation is too short and it’s easy to stumble on past/future, + - N and ! and get lost.

Tip: In the past I realised a single days operator in effect pivots around today. Eg “from -14 to today”.

Either we need much better documentation if possible or to implement a better solutions.

True, I must confess I kind of gave up on that part of the days operator doc because the preceding line of doc was so opaque to me… (I find the code easier to understand than the following line of doc - my head kind of goes pooooffffbang!!! whenever I try and read it :smiley:)…

output those input tiddlers in which field F is D days in the { future | past } or any time { before | after } that, including { past | future }

So after reading the doc once and being bewildered by that line of doc I have on subsequent visits tended to short skip to the Examples as these were easier to understand, I have to confess I had become somewhat blind to that final line of doc which is much less opaque than the one above…I agree.

! output those input tiddlers in which field F is at least D days in the { future past }

So I guess you are right - the code does perform according to the current doc but not according to the examples cited in the doc.

The remaining thing is whether the current (mostly corrected documented :smiley: ) behaviour is actually useful?

I would advocate that if both !prefix and non-prefix filter statements are used in the same code then binary decision making is more desirable because it allows a sequence of mutually exclusive filter expressions to combine to effectively form an IF ELSE block which is a fairly fundamental requirement in most algorithms, the ‘!’ operator usually allows us to code mutual exclusivity in filters so why allow this as an exception?

Not sure I agree with the pivot bit I see it as two dates on a timeline - for me it bottoms out as the subtraction of two UTC dates with time component (hrs min secs) removed to form an interval expressed in days and then comparison of that interval length with another interval length - nothing special about today - merely one end of an interval used in integer interval arithmetic. :smiley:

It looks as though it’s not exclusive. For instance, today, I can run this on http://tiddlywiki.com:

[days:created[-94]] +[!days:created[-94]]

(testing this in the few days after this message might mean increasing the two 94's to 95 or 96)

and I get a few values:

which were modified 4th March 2023

But these are exclusive:

[days:created[-94]] +[!days:created[-95]]
<!--           ^^                    ^^     -->

and return no values.

So I think you will want to use two different values like that.

I don’t know if this was the best call for TW to make, but it’s a difficult decision and probably too late to change it.

Yup you are probably right.

Anyway…

<!-- Add red bell 'overdue' button if tiddler is overdue a review -->
<$list filter='[all[current]!is[system]!tag[z hidden]has:field[last-reviewed3]!days:last-reviewed3{!!interval3}]'>
<$set name="intervalReview" value={{{ [{!!interval3}abs[]] }}}>
<<reviewOverdueButton>>
</$set>
</$list>

I need to be able to change the line

<$list filter='[all[current]!is[system]!tag[z hidden]has:field[last-reviewed3]!days:last-reviewed3{!!interval3}]'>

so that it the days operator receives an argument that is interval3 - 1

interval3 is already stored as a negative integer so I need -45 to become -46

Playing around with syntax as usual,

For now the red and green icon both appear on tiddlers due for review ‘today’.