EvanHahn / HumanizeDuration.js

361000 becomes "6 minutes, 1 second"
https://evanhahn.github.io/HumanizeDuration.js/
The Unlicense
1.64k stars 175 forks source link

Custom number rendering feature #112

Closed robokozo closed 6 years ago

robokozo commented 7 years ago

As discussed here:

https://github.com/EvanHahn/HumanizeDuration.js/issues/110

I added the ability to include custom number renderers on creation of the humanizer.

The only 'issue' that I couldn't cover was that it's an all or nothing approach. Every possible unit that is used will need a custom renderer. Missing a renderer will throw and error when trying to render.

The default renderers don't change any existing functionality.

There was potential conflicts with custom decimal markers, but I covered that case and wrote a unit test as well.

EvanHahn commented 7 years ago

I like this so much that I could see this replacing some of the existing options we have, or at least making them more generalized.

function leftPad (n, size, pad) {
  var result = String(n)
  while (result.length < size) {
    result = pad + result
  }
  return result
}

humanizeDuration(22140000, {
  renderUnit: function (options) {
    if (options.unit === 'm') {
      return options.amount + ' little ' + options.renderedUnit
    } else {
      return leftPad(options.amount, 2, '0') + ' ' + options.renderedUnit
   }
  }
})
// => 06 hours and 9 little minutes

This would allow a fair bit of flexibility and might enable things like #110, #109, #74, and #73. It might also "obsolete" some existing features like decimal, though we'd probably leave those in for convenience.

What do you think?

robokozo commented 7 years ago

I agree 100%! Being able to control how the numbers are rendered grants a ton of flexibility.

I think it'd be nice to keep features like decimal for simplicity at the cost of increased 'complexity' in the code.

EvanHahn commented 7 years ago

Spent some more time thinking about this, and I'm a bit stuck.

Let's say the API looked something like this:

// Renders "6 HOURS, 9 MINUTES"
humanizeDuration(22140000, {
  renderUnit: function (options) {
    return options.amount + ' ' + options.renderedUnit.toUppercase()
  },
  renderResult: function (renderedUnits) {
    return renderedUnits.join(', ')
  }
})

In theory, this means that many of the other options are conveniences: delimiter, spacer, decimal, conjunction, serialComma, and maybe even language to some extent. These options would effectively create new default renderUnit or renderResult functions. For example, the following two would become equivalent:

humanizeDuration(123456, { conjunction: ' and ' })

humanizeDuration(123456, {
  renderResult: function (renderedUnits) {
    return renderedUnits.join(' and ')
  }
})

At first, it seems like we can make round convenience option, which enables lots of features like custom rounding or keeping of zeroes in the output. Developers would put their custom rounding into renderUnit.

But the problem is what happens when rounding changes the final result. A decision made in one renderUnit might affect an earlier rendering.

For example, without rounding, humanizeDuration(97320999) renders 1 day, 3 hours, 2 minutes, 0.999 seconds. But with rounding (which would be customizable in this new design), the result could vary. If you decided to round up, it'd be 1 day, 3 hours, 3 minutes. Or you could round down and get 1 day, 3 hours, 2 minutes. Or you could decide a bunch of other things, which is fine, but a decision made in one renderUnit might affect an earlier one.

I can think of ways to get around this, but they're not elegant.

Do you have any thoughts?

Dragory commented 7 years ago

I know this pull request hasn't been active for a while, but I think having 3 functions would solve the issue @EvanHahn: preprocess, renderUnit, renderResult, where preprocess takes care of rounding etc. (and choosing only the largest X for instance).

Example:

humanizeDuration(123456, {
  preprocess (units) {
     // unit e.g. {type: 'm', amount: 50}, no need for language data here yet
     // Rounding logic, "largest" logic, etc. would be here
  },

  renderUnit (unit) {
    return unit.amount + ' ' + unit.label.toUppercase()
  },

  renderResult (renderedUnits) {
    return renderedUnits.join(' and ')
  }
})

Most of the current settings would then basically just be shorthands for these 3 functions.

Edit: I can make a pull request with these features later for consideration

EvanHahn commented 7 years ago

@Dragory I think I like this.

What would preprocess return?

EvanHahn commented 6 years ago

There hasn't been movement on this in months and this module is in maintenance mode and won't be accepting new features (see #120), so I'm going to close this PR.