luxon icon indicating copy to clipboard operation
luxon copied to clipboard

More control over DateTime#toRelative rounding behavior

Open corymharper opened this issue 3 years ago • 6 comments

Is your feature request related to a problem? Please describe. For some units of time, rounding down is very unintuitive. It's probably fine if 29.99 minutes rounds down to 29, but it's fairly inaccurate to describe 1.99 years away as "1 year away." Padding in these situations that could be minutes away or years away can lead to a lot of extra code to figure out how much padding there should be in this situation.

Describe the solution you'd like I think one possible solution would be to change the round option you can pass to toRelative from a on/off style boolean into a setting probably based on a string. Something like round: 'down' | 'up' | 'closest' | 'off' where, if we want to keep the current default, it defaults to down. off would be the equivalent of false currently, up would invert the behavior and always round upwards, and closest would round towards the closest integer, rather than specifically up or down.

Describe alternatives you've considered With the existing options the only alternatives I've come up with are calculating the padding based on the kind of rounding I want based on the difference between the date for comparison and now, or altering the date for comparison itself to generate the term I want when handed to toRelative. An alternative to the addition I suggest above might be to allow handing a function to the round option that would receive a parameter of the unrounded relative value, and the resulting number would be whatever is returned from that function.

corymharper avatar Jan 28 '22 20:01 corymharper

This "rounding down" behavior is especially strange when using toRelative() to show a countdown timer.

With the default behavior, you'll see "1 minute" for a full 60 seconds before you see "59 seconds" and realize now you've actually got a minute left.

I was hopeful when I saw https://github.com/moment/luxon/pull/926 because I thought specifying "multiple units" like ["minutes", "seconds"] would allow me to see output like in 1 minute 23 seconds, but that's not how that behaves. (It continues to pick a single unit, from among those choices.)

So maybe an option like significantUnits would help here. (Please feel free to tell me to open a separate issue for this if you'd like to consider it separately. I'm mostly adding it to this ticket for some example use cases.) To take the above 29.99 minutes as an example, I'd expect .toRelative({significantUnits: 2}) to output in 29 minutes 59 seconds.

NfNitLoop avatar Apr 17 '22 19:04 NfNitLoop

I ended up implementing my own. If someone wants to fold that into Luxon as a feature, I'll be happy to use it from there. :)

function relativeDuration(duration: Duration, opts?: Opts): string {
    let sigU = opts?.significantUnits ?? 2
    if (sigU < 1) {
        throw Error("Signficant units can't be < 1")
    }

    let units = opts?.units ?? defaultUnits
    // Make sure units are ordered in descending significance:
    units = allUnits.filter(it => units.includes(it))

    
    let negative = duration.valueOf() < 0
    if (negative) { duration = duration.negate() }
    duration = duration.shiftTo(...units)

    // Remove unnecessary most-significant units:
    while (units.length > 1) {
        if (duration.get(units[0]) > 0) {
            // we've found the most significant unit:
            break
        }

        units = units.slice(1)
        duration = duration.shiftTo(...units)
    }

    units = units.slice(0, sigU)
    duration = duration.shiftTo(...units)
    // If the last unit has fractional bits, we don't care. We're explicitly limiting significant units:
    let lastUnit = units[units.length - 1]
    duration = duration.set({
        [lastUnit]: Math.floor(duration.get(lastUnit))
    })

    let relative = duration.toHuman()
    if (negative) {
        return `${relative} ago`
    } else {
        return `in ${relative}`
    }
}

interface Opts {
    // Default: 2
    significantUnits?: number

    // Default: all but quarters & months
    units?: (keyof DurationObjectUnits)[]
}

const allUnits: ReadonlyArray<keyof DurationObjectUnits> = ["years", "quarters", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds"]
// No quarters/weeks:
const defaultUnits: ReadonlyArray<keyof DurationObjectUnits> = ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds"]

NfNitLoop avatar Apr 18 '22 00:04 NfNitLoop

I thought specifying "multiple units" like ["minutes", "seconds"] would allow me to see output like in 1 minute 23 seconds

I also interpreted it to mean that, and I'm currently lacking a way to format a relative date in that way, so it'd be nice to see this feature added.

sphinx9 avatar May 23 '22 07:05 sphinx9

At least make round mean round(), not floor().

steveschmitt avatar Jul 13 '22 22:07 steveschmitt

I think this or something similar would be a great addition. Especially when moving from moment where the default behavior was different.

const moment = require('moment') 
const luxon = require('luxon')
const halfdayPadding = 1000 * 60 * 60 * 12;
moment('2023-08-25T15:38:06.733Z').fromNow() === luxon.DateTime.fromISO('2023-08-25T15:38:06.733Z').toRelative({ round: true, padding: halfdayPadding })

Having to add this halfday padding constant doesn't seem very ergonomic.

moment('2023-08-25T15:38:06.733Z').fromNow() === luxon.DateTime.fromISO('2023-08-25T15:38:06.733Z').toRelative({ round: 'up' })

Would be much more graceful.

SeanDunford avatar Aug 29 '23 15:08 SeanDunford

@icambron Sorry for the tag, you're just the first maintainer I encountered. I'd be willing to contribute this change myself, but considering it's a change that as described would break existing functionality (changing the boolean option to a series of strings), I didn't want just to do the work and make a pull request without having an indication of if the project owners had any interest in this change to begin with. I'm tagging you to see if I can get some feedback there, or if maybe with some tweaks such a change would be accepted.

corymharper avatar Aug 29 '23 21:08 corymharper