Skip to content

Conversation

@apoelstra
Copy link

@apoelstra apoelstra commented Dec 27, 2025

Currently in taskwarrior if you attempt to set a negative duration for a UDA, the UDA will appear to be set to PT0S (zero seconds). What is actually happening is a bit weird. Let's consider the example where the user types '-80s'.

  1. First, Duration::parse_units will correctly read this as -80 seconds.
  2. Then, Duration::formatISO will write this out as PT-1M-20S, which is what gets stored in the database.
  3. Later, when the user attempts to read this value, Duration::parse will fail, and the default value of 0 will be output.

In my view, we should either reject negative durations when the user first enters them, or this round-trip should succeed. I would kinda like to have negative durations, so this PR makes it succeed, by extending the ISO parser to allow negative entries for times.

This approach has the benefit that users who have already accidentally put negative durations into their taskwarrior db will have these entries start working (though this may be perceived as a surprising/breaking change). It has the drawback that we're defining an ad-hoc extension to the ISO format and blessing it in our parser, making it a breaking change to revert this in the future. (Re "ad-hoc" -- I read through ISO 8601 and it does not allow this, or negative durations of any form for that matter. Also, our current code produces this output by using the modulo operator on negative numbers, which used to be implementation defined behavior) (though this SE post claims that C++11 defines it) (I did not check -- reading ISO 8601 is one thing; the C++ standard is another :)).

It also has the drawback that users will see durations like PT-1H-56M-9S in their taskwarrior output, which to my eye looks like a bug, or at least an unintentionally exposed implementation detail.

An alternative approach would be to extend the duration format by adding an optional +/- sign before the P. According to this MDN page this is defined by ECMAScript (lol also not reading that standard). The benefit here is: we're following a standard (assuming the MDN doc is correct); no existing timewarrior databases are likely to contain durations of this form, reducing the chances of "surprise" behavior changes; the resulting output looks much nicer.

It's also easier to implement, though I need to modify both parse() and formatISO(). I have done it on a private branch -- let me know and I will close this PR and open one with this alternative approach.

Currently in taskwarrior if you attempt to set a negative duration for
a UDA, the UDA will appear to be set to PT0S (zero seconds). What is
actually happening is a bit weird. Let's consider the example where
the user types '-80s'.

1. First, Duration::parse_units will correctly read this as -80 seconds.
2. Then, Duration::formatISO will write this out as PT-1M-20S, which is
   what gets stored in the database.
3. Later, when the user attempts to read this value, Duration::parse
   will fail, and the default value of 0 will be output.

In my view, we should either reject negative durations when the user first
enters them, or this round-trip should succeed. I would kinda like to have
negative durations, so this PR makes it succeed, by extending the ISO parser
to allow negative entries for times.

This approach has the benefit that users who have already accidentally put
negative durations into their taskwarrior db will have these entries start
working (though this may be perceived as a surprising/breaking change). It
has the drawback that we're defining an ad-hoc extension to the ISO format
and blessing it in our parser, making it a breaking change to revert this
in the future. (Re "ad-hoc" -- I read through ISO 8601 and it does not
allow this, or negative durations of any form for that matter. Also, our
current code produces this output by using the modulo operator on negative
numbers, which used to be implementation defined behavior) (though
[this SE post](https://stackoverflow.com/questions/7594508/why-does-the-modulo-operator-result-in-negative-values)
claims that C++11 defines it).

It also has the drawback that users will see durations like PT-1H-56M-9S
in their taskwarrior output, which to my eye looks like a bug, or at least
an unintentionally exposed implementation detail.

An alternative approach would be to extend the duration format by adding
an optional +/- sign before the P. According to [this MDN page](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration)
this is defined by ECMAScript. The benefit here is: we're following a
standard (assuming the MDN doc is correct); no existing timewarrior
databases are likely to contain durations of this form, reducing the
chances of "surprise" behavior changes; the resulting output looks much
nicer.

It's also easier to implement, though I need to modify both parse() and
formatISO(). I have done it on a private branch -- let me know and I will
close this PR and open one with this alternative approach.
apoelstra added a commit to apoelstra/gbf-libshared that referenced this pull request Dec 27, 2025
…in ISO format

According to [this MDN document](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration)
there is an ECMAScript extension to ISO 8601 to allow signed durations by putting
a + or - before the ISO duration format.

Internally we support signed durations -- we will parse "-60w" or "-60min",
which we store in a `time_t` (whose signedness is technically implementation
defined, but which is signed on all major compilers). However, when formatting
these as ISO 8601 we get get garbed output like PT-1H-56M-9S, which we cannot
parse and probably neither can anything else. (Taskwarrior, when asked to
assign a negative duration to a duration-typed UDA, will store this garbled
output but then reproduce it as PT0S, an unfortunate user experience.)

This PR updates `Duration::formatISO` to instead prepend a '-' before negative
durations, and updates `Duration::parse_designated` to parse such things.

Alternative to GothenburgBitFactory#110, which simply blesses the garbled format by extending the
parser to support it.
@apoelstra
Copy link
Author

Actually I just opened an alternative -- #111.

Please close at least one of these.

apoelstra added a commit to apoelstra/gbf-libshared that referenced this pull request Dec 27, 2025
…in ISO format

According to [this MDN document](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration)
there is an ECMAScript extension to ISO 8601 to allow signed durations by putting
a + or - before the ISO duration format.

Internally we support signed durations -- we will parse "-60w" or "-60min",
which we store in a `time_t` (whose signedness is technically implementation
defined, but which is signed on all major compilers). However, when formatting
these as ISO 8601 we get get garbed output like PT-1H-56M-9S, which we cannot
parse and probably neither can anything else. (Taskwarrior, when asked to
assign a negative duration to a duration-typed UDA, will store this garbled
output but then reproduce it as PT0S, an unfortunate user experience.)

This PR updates `Duration::formatISO` to instead prepend a '-' before negative
durations, and updates `Duration::parse_designated` to parse such things.

Alternative to GothenburgBitFactory#110, which simply blesses the garbled format by extending the
parser to support it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant