Moment: Support invalid durations

Created on 28 Jul 2014  ·  44Comments  ·  Source: moment/moment

There is currently no way to determine whether a duration parse was successful, even by inspecting the fields.

New Feature

Most helpful comment

Are there any updates on this?

With moment 2.23.0 the isValid() function also return true when trying to create a duration from an invalid ISO8601 string.

Example:

const mom = moment.duration('asdf')
console.log(mom.isValid()) // This returns true, expected would be false 

Its kinda annoying imo. and completely defeats the purpose of the isValid() function on Duration objects (is there even a case, when isValid() returns false since moment does interpret even an invalid input?)

All 44 comments

I agree. We need an isValid method as a start.

I think invalid durations should throw exceptions and the current handling of invalid durations is a bug not an enhancement. Consider these cases:

`var wrong = moment.duration(3,'mintues');`

What will the result be? The docs don't define what happens on bad input.

This has a cascading effect, as duration is used instead of add() and subtract(), so these have undefined behavior as well:

var hmm = moment().subtract(3,'mintues').toDate();
var uhoh = moment().add(3,'mintues').toDate();

From manual testing I can tell you what happens: moment "succeeds" by using a duration of zero.

If moment() had simply thrown an exception on bad input, these issue would have been identified immediately. Since it silently "succeeded", a class Garbage-In, Garbage-Out bug persisted.

Since the behavior of typo'ed duration is currently undefined, starting to throw exceptions on bad strings would be backwards compatible.

If a string is being used that is not trusted that may have a typo, try/catch can be used explicitly when testing if the string can be used to describe a valid duration.

+1 @markstos.

What about supporting a strict option (like moment itself for dates) that throws? Returning a duration of zero when parsing fails is quite dangerous.

A strict option would be better than no change, but I don't think parsing invalid dates as zero duration is good default behavior in the first place.

Agreed. Would a possible change only land with [email protected]?

Why not use the same pattern as the rest of moment? Have an _isValid field and an isValid() method and set a duration as invalid if the parsing fails.

For reference, here are the other documented places where isValid() is used:

https://github.com/moment/momentjs.com/search?utf8=%E2%9C%93&q=isValid

Linking to documentation not the code was intentional, though the code mentions are relevant too.

+1
An ability to find out whether parsing was successful or not is very needed.
If backward compatibility is a concern then there could be possible to introduce a method Duration.parse(input: string, strict?: boolean = true): Duration which would throw if input is incorrect and strict argument was specified.
The current behavior when we're getting a Duration with all zeros for arbitrary data is very weird.

+1
Having an isValid() method on a duration (to see if parsing was successful) would be handy

Hi all, Moment 2.18.0 now has an .isValid method for durations. However, it's pretty lenient. @markstos @theazureshadow and others, do try it out and let us know if you have suggestions.

@marwahaha,

As an initial test, I copy/pasted one of the examples above. Instead of returning true or false as expected, it throws an exception (with moment 2.18.0):

 moment.duration(3,'mintues').isValid();

(Created the invalid duration still "succeeds" without throwing an exception, but now checking to see if it's valid throws an exception! )

Although it appears there aren't docs for the new method yet, so perhaps this isn't the intended use.

@markstos I just ran that code in the console of Momentjs.com - which has 2.18.0 - without an issue. Can we get more details on that?

Perhaps, that code should be invalid because minutes is spelled wrong.

On Mar 21, 2017 2:35 PM, "Maggie Pint" notifications@github.com wrote:

@markstos https://github.com/markstos I just ran that code in the
console of Momentjs.com - which has 2.18.0 - without an issue. Can we get
more details on that?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/moment/moment/issues/1805#issuecomment-288176838, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ACbGmWIf24KlRs8oiS2wTn206iJNMod7ks5roBhfgaJpZM4CRuVM
.

@maggiepint @marwahaha Yes, the misspelling of "minutes" IS why the duration should be invalid.

@markstos I can't reproduce that exception in the console of Momentjs.com either. (That's 2.18.1 now, though.) If you can still reproduce the exception when calling isValid, can we get more details?

screen shot 2017-03-22 at 10 38 06

(Obviously Mark wanted isValid to return false in this situation, but that's a separate issue from the exception. It's not 100% obvious to me how we should handle parsing validity with object inputs.)

I can't reproduce the exception either with 2.18.1. Perhaps it was a false alarm. I can reproduce @butterflyhug's result of having this kind of invalid duration returning "true" for "isValid()".

Shouldn't parsing fail in this case instead of returning "0 minutes" as a wrongly-valid output as the result?

Should is a different question, but IIRC what it does is interpret that
value as milliseconds because the unit isn't found in the unit hashtable.

Should it be invalid? IMO probably.

On Mar 22, 2017 8:42 AM, "Mark Stosberg" notifications@github.com wrote:

I can't reproduce the exception either with 2.18.1. Perhaps it was a false
alarm. I can reproduce @butterflyhug https://github.com/butterflyhug's
result of having this kind of invalid duration returning "true" for
"isValid()".

Shouldn't parsing fail in this case instead of returning "0 minutes" as a
wrongly-valid output as the result?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/moment/moment/issues/1805#issuecomment-288440827, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AFxi0nZAz8gwAJM8fVx5rYPyIWjeEfHMks5roUF4gaJpZM4CRuVM
.

There appears to be a bug here:

https://github.com/moment/moment/blob/497f918515ae6ab900f008f19523b1e4ae5e2450/src/lib/duration/create.js#L34

An assumption is made that there the string is valid and appears in the "duration" map. It seems like the fix is to add a check here to confirm that the string is a valid value to set. If not, set _isValid:false

This behavior is consistent with the standard isValid method:

moment({'mintues': 3}).isValid()
> true

The source of this is normalizeObjectUnits https://github.com/moment/moment/blob/497f918515ae6ab900f008f19523b1e4ae5e2450/src/lib/units/aliases.js#L14-L29 which only adds valid attributes (and drops the offending ones).

@maggiepint @ichernev should this behavior change? We are doing a breaking release soon...

Converting '3 mintues' to '0 minutes' and declaring the result "valid" is a bug. It may be a be a breaking change to fix it, but it's still a bug fix.

I agree that this validation behavior isn't ideal, but I disagree that this is necessarily a bug.

Consider the case where you parse something like moment({'minutes': 3, '$cacheKey': 92619502}).isValid(). I strongly expect that we have users who appreciate having that parse as a valid moment, and I don't see a principled distinction between this example and the bug that you wrote.

The API we are discussing the is "object parser" for the moment constructor, which is documented here:

http://momentjs.com/docs/#/parsing/object/

The documentation is currently silent on the treatment of passing unknown or additional arguments.

Right now, the behavior of Moment here can be described as "Garbage In, Garbage out". Garbage input is silently accepted and the result is "Garbage out"-- misspelled dates being converted to other dates and being considered valid!

These updates would allow a developer find out as soon as possible that they've made a mistake:

  • Document that an exception will be thrown in unknown arguments are passed to the object constructor.

    • Throw an exception if unknown arguments are passed to the object constructor

As long as Moment continues to accept "Garbage In" as valid, it will continue to be a disservice to developers and users are left with Moment wrongly reporting that broken dates are "valid".

This improvement seems well worth a "breaking" change.

I was just bitten by this: moment().subtract('1 day')... this is not a valid way to express a duration. I would not expect it to be equivalent to .subtract(0) though. IMO, the least-suprising thing to do in this case is to throw.

@johnvh I agree completely.

You know it's not a "valid way to express a duration" yet you write that code anyway? What do you expect? I assume you worked out it was not a valid way to express a duration from the excellent documentation momentjs authors provide?

On 25/04/2017, at 08:29, Mark Stosberg notifications@github.com wrote:

@johnvh I agree completely.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.

@simonfox developers are human. We make mistakes. That's why half our code base is tests. The Moment documentation /is/ excellent, but it doesn't help if you believe you've remembered the API correctly and don't need to reference the documentation. That's why it's helpful to not only have great documentation but also code that fails on invalid input instead of silently converting invalid input into broken "valid" input. If @johnvh had immediately received a validation failure when his code ran, he could have checked the excellent documentation to see what the problem might be. Unfortunately, Moment treated invalid input as valid and raised to no alarm.

@markstos exactly. I agree 100%.

This was a bug that had to be tracked down. We didn't write it that way knowing it was invalid of course. It looks right, as the moment manipulation methods are very liberal in their supported signatures. We also use agenda in the same project, which uses human-interval for converting natural language into durations. So we'll have something like this only lines apart:

moment().subtract('1 day');
agenda.processEvery('1 day');

The latter works as intended, the former does not. If the former raised an exception, it would have alerted us to our mistake.

Also, the fact that Moment /has/ an isValid() method implies that it validates values as either valid or invalid. In cases where invalid values are classified as valid, the isValid() method appears not to work as documented.

Hi all, we'd love to review any PRs in this area!

I ran into this bug, or something similar to it. If I run moment.duration('3', 'minutes').asMinutes() using Moment 2.18.1, I get a result of 0. I think that should either return 3 or throw an exception. At the very least, moment.duration('3', 'minutes').isValid() should return false.

The way Moment behaves now can cause a lot of trouble by causing bugs that may be hard to detect and diagnose, as several people here have mentioned.

Has this been addressed?

@TomJSmith A quick test shows the bug still exists. This code:

 moment().subtract('1 day');

"succeeds", subtracting zero days instead of one day as expected. You can call .isValid() on the resulting date and get a true response back indicating the result is valid when it is not.

Current behaviour could lead to bad consequences in some use cases and should be clearly documented with a warning:

Use case
I want to delete resources based on a retention duration:

const duration = moment.duration('invalid');
const currentDate = moment();
const removeBeforeDate  = moment().subtract(duration);

console.log(`currentDate      : ${currentDate}`);
console.log(`removeBeforeDate : ${removeBeforeDate}`);

Result:

currentDate      : Fri Apr 13 2018 13:15:04 GMT+0200
removeBeforeDate : Fri Apr 13 2018 13:15:04 GMT+0200

Consequence:
Every resources up to the current date are deleted...

So far, the only way I found to workaround this if to test the value after the parsing:

const duration = moment.duration('invalid');
if (duration.toISOString() === 'P0D') {
    // throw an Error
}

Yes, this bug is bad and has been open since 2014. This is something that the "date-fns" projects gets right. They actually tell you if invalid input is invalid. https://date-fns.org/ Unfortunately, date-fns doesn't handle time zone conversions, but perhaps there's a way to handle those with an external library.

For the Moment project to fix this, they have to quit accepting garbage input and treating it as a valid date. While this seems like a better behavior, it's technically a "backwards breaking behavior" change, so it seems there's resistance to make the change. But given the project has not taken action in the last nearly four years, I wouldn't expect a fix soon.

Are there any updates on this?

With moment 2.23.0 the isValid() function also return true when trying to create a duration from an invalid ISO8601 string.

Example:

const mom = moment.duration('asdf')
console.log(mom.isValid()) // This returns true, expected would be false 

Its kinda annoying imo. and completely defeats the purpose of the isValid() function on Duration objects (is there even a case, when isValid() returns false since moment does interpret even an invalid input?)

@robbiecloset The update is since my last post in April, the "date-fns" project has gotten better at time zone support. There are now "date-fns-timezone" and "date-fns-tz" projects to help with this.

I would consider using it if it's feasible to switch.

I use moment.duration(value).toISOString() === value, that value is read from a configuration.

I use moment.duration(value).toISOString() === value, that value is read from a configuration.

@bors-ltd I think part of the problem is that moment might be able to decode two values that produce the same encoded value.

For example: moment.duration('PT0M0S').toISOString() === moment.duration('PT0S').toISOString().

If you look at the tests, it seems like invalid values are actually apart of the specification:

https://github.com/moment/moment/blob/2.24.0/src/test/moment/duration.js#L419-L420

Patterns that do not match the ISO duration standard are being tested as being "0" seconds.

Hello, we also had the same problem with the isValid() method and end up doing it manually.
const isValidDuration = duration => { return !!duration.match( /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/ ); };
Hope it could help anyone.

See https://momentjs.com/docs/#/-project-status/

Thanks for all the discussion here.

A hacky fix is to replace the isValid method with a method that assures that the sum of the data parts is > 0 (or at least likely invalid)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Shoroh picture Shoroh  ·  3Comments

danieljsinclair picture danieljsinclair  ·  3Comments

nikocraft picture nikocraft  ·  3Comments

paulyoung picture paulyoung  ·  3Comments

alvarotrigo picture alvarotrigo  ·  3Comments