question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

`Duration#toHuman` does not print human-readable strings

See original GitHub issue

Based on the name I would expect that Duration#toHuman returns strings that are optimal for human reading. But, IMHO this is not the case.

The way I see it there are three issues.

  • If a Duration is created from a number of milliseconds then the milliseconds are printed as-is, which is not human-readable.
Duration.fromMillis(22140000).toHuman() //=> "22140000 milliseconds"

In this case, I would expect something like the string “6 hours and 9 minutes” which is what humanize-duration returns.

  • If a Duration is created with some units being zero then those units are printed even though they do not benefit humans.
Duration.fromMillis(3 * 60 * 1000).shiftTo("hours", "minutes").toHuman()
//=> "0 hours, 3 minutes"
Duration.fromObject({ days: 0, hours: 0, minutes: 12, seconds: 0 }).toHuman()
//=> "0 days, 0 hours, 12 minutes, 0 seconds"
  • If a Duration is created with an empty object then an empty string is returned. An empty string is not a human-readable duration.
Duration.fromObject({}).toHuman() //=> ""

It seems to me that toHuman only returns the internal representation of the duration with unit names attached and not actual human-friendly output.

I think that either the toHuman method should be renamed as the current name is confusing or the above problems should be fixed. Units that are zero should not be printed and small units should be converted into larger units if possible. To fix the last issue I think that toHuman should accept a (potentially optional) second argument that specifies the smallest unit to print. This argument can be used to not print unwanted precision (for instance milliseconds) and will be used as the fallback unit to print if the duration contains no units.

Here is a small proof of concept of what I suggest:

function toHuman(dur: Duration, smallestUnit = "seconds"): string {
  const units = ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds", ];
  const smallestIdx = units.indexOf(smallestUnit);
  const entries = Object.entries(
    dur.shiftTo(...units).normalize().toObject()
  ).filter(([_unit, amount], idx) => amount > 0 && idx <= smallestIdx);
  const dur2 = Duration.fromObject(
    entries.length === 0 ? { [smallestUnit]: 0 } : Object.fromEntries(entries)
  );
  return dur2.toHuman();
}

For the above examples this implementation behaves as follows:

toHuman(Duration.fromMillis(3 * 60 * 1000)) //=> "3 minutes"
toHuman(Duration.fromObject({ days: 0, hours: 0, minutes: 12, seconds: 0 })) //=> "12 minutes"
toHuman(Duration.fromObject({})) //=> "0 seconds"

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:67
  • Comments:27 (4 by maintainers)

github_iconTop GitHub Comments

63reactions
orphic-lacunacommented, Feb 10, 2022

Additional to your modified toHuman() method I’d like to go even further. In my opinion the .toHuman() method should have the following behaviour, some examples:

Duration.fromObject({seconds: 180}).toHuman() => "3 minutes and 0 seconds"
Duration.fromObject({seconds: 181}).toHuman() => "3 minutes and 1 second"
Duration.fromObject({seconds: 3600}).toHuman() => "1 hour, 0 minutes and 0 seconds"
Duration.fromObject({seconds: 3601}).toHuman() => "1 hour, 0 minutes and 1 seconds"
Duration.fromObject({seconds: 3800}).toHuman() => "1 hour, 3 minutes and 20 seconds"
Duration.fromObject({seconds: 3800}).toHuman({ unitDisplay: "short" }) => "1 hr, 3 mins, 20 secs"

And I would like to see some new options for the .toHuman() method (with examples):

Config Option: stripZeroUnits

Removes all zero parts from the human-readable string. Allowed values are all which would remove all parts that are zero, or end which would only remove zero parts at the end of the text. Examples:

Duration.fromObject({seconds: 180}).toHuman({stripZeroUnits: "end"}) => "3 minutes"
Duration.fromObject({seconds: 3660}).toHuman({stripZeroUnits: "end"}) => "1 hour and 1 minute"
Duration.fromObject({seconds: 3601}).toHuman({stripZeroUnits: "all"}) => "1 hour and 1 second"

Config Option: precision

Determines a minimum precision of the human-readable string. All parts of it which are smaller than the specified precision will be omitted. Examples:

Duration.fromObject({seconds: 3800}).toHuman({precision: {minutes: 5}}) => "1 hour"
Duration.fromObject({seconds: 3800}).toHuman({precision: {minutes: 2}}) => "1 hour and 3 minutes"

Config Option: maxUnits

An integer that determines the maximum allowed number of parts. Examples:

Duration.fromObject({seconds: 3661}).toHuman({maxUnits: 2}) => "1 hour and 1 minute"

Config Options: smallestUnit and biggestUnit

Determine the biggest and smallest unit that should be used. Default values are smallestUnit: "seconds" and biggestUnit: "years". Examples:

Duration.fromObject({seconds: 3661}).toHuman({biggestUnit: "minutes"}) => "61 minutes and 1 second"
Duration.fromObject({seconds: 3661}).toHuman({smallestUnit: "hours"}) => "1 hour"

To achieve this behaviour I have written the following wrapper function for the .toHuman() method:

const Duration = luxon.Duration;
Duration.prototype.__toHuman__ = Duration.prototype.toHuman;
Duration.prototype.toHuman = function(opts = {}) {
	let duration = this.normalize();
	let durationUnits = [];
	let precision;
	if (typeof opts.precision == "object") {
		precision = Duration.fromObject(opts.precision);
	}
	let remainingDuration = duration;
	//list of all available units
	const allUnits = ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds"];
	let smallestUnitIndex;
	let biggestUnitIndex;
	// check if user has specified a smallest unit that should be displayed
	if (opts.smallestUnit) {
		smallestUnitIndex = allUnits.indexOf(opts.smallestUnit);
	}
	// check if user has specified a biggest unit
	if (opts.biggestUnit) {
		biggestUnitIndex = allUnits.indexOf(opts.biggestUnit);
	}
	// use seconds and years as default for smallest and biggest unit
	if (!((smallestUnitIndex >= 0) && (smallestUnitIndex < allUnits.length))) smallestUnitIndex = allUnits.indexOf("seconds");
	if (!((biggestUnitIndex <= smallestUnitIndex) && (biggestUnitIndex < allUnits.length))) biggestUnitIndex = allUnits.indexOf("years");
	 
	for (let unit of allUnits.slice(biggestUnitIndex, smallestUnitIndex + 1)) {
		const durationInUnit = remainingDuration.as(unit);
		if (durationInUnit >= 1) {
			durationUnits.push(unit);
			let tmp = {};
			tmp[unit] = Math.floor(remainingDuration.as(unit));
			remainingDuration = remainingDuration.minus(Duration.fromObject(tmp)).normalize();

			// check if remaining duration is smaller than precision
			if (remainingDuration < precision) {
				// ok, we're allowed to remove the remaining parts and to round the current unit
				break;
			}
		}
		
		// check if we have already the maximum count of units allowed
		if (durationUnits.length >= opts.maxUnits) {
			break;
		}
	}
	// after gathering of units that shall be displayed has finished, remove the remaining duration to avoid non-integers
	duration = duration.minus(remainingDuration).normalize();
	duration = duration.shiftTo(...durationUnits);
	if (opts.stripZeroUnits == "all") {
		durationUnits = durationUnits.filter(unit => duration.values[unit] > 0);
	} else if (opts.stripZeroUnits == "end") {
		let mayStrip = true;
		durationUnits = durationUnits.reverse().filter((unit, index) => {
			if (!mayStrip) return true;
			if (duration.values[unit] == 0) {
				return false;
			} else {
				mayStrip = false;
			}
			return true;
		});
	}
	return duration.shiftTo(...durationUnits).__toHuman__(opts);
}

According to the contribution guidelines one should ask first if Luxon wants to integrate this or that feature before putting too much effort into this … So here it is:

Do you think this feature set should be included into Luxon? If so, I could make a pull request if you want.

6reactions
luketynancommented, Aug 2, 2022

Hi there,

I’d just like to bump this issue. I would love to see some of the changes proposed by @orphic-lacuna and @paldepind, especially those focused on stripping 0 values and dealing with them within the toHuman() function. Specifically elegantly resolving the following:

Duration.fromObject({ days: 0, hours: 0, minutes: 75 }).toHuman() // => 0 days, 1 hours, 15 minutes, 0 seconds

It looks like this is a relatively old PR/issue. Are there any updates or other info on this being implemented soon?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Print a Duration in human readable format by EL
toString() returns a human readable vale), so I can just write ${startTime} and everything goes in the right way (e.g. it is rendered...
Read more >
Text function in Power Apps - Power Platform - Microsoft Learn
Converts any value and formats a number or date/time value to a string of text. Description. The Text function formats a number or...
Read more >
4. Printing Output - Effective awk Programming, 3rd Edition ...
Numeric values are converted to strings and then printed. The simple statement print with no items is equivalent to print $0 : it...
Read more >
Protocol Buffer Basics: C++ - Google Developers
Over time, this is a fragile approach, as the receiving/reading code must be ... string DebugString() const; : returns a human-readable representation of ......
Read more >
tshark(1) Manual Page - Wireshark
It lets you capture packet data from a live network, or read packets from a ... If the zlib library is not present...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found