From 4a16d4ce5f144509d8c930344fa393849dddcb25 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 12 Mar 2020 11:25:21 +0100 Subject: [PATCH] feat(core): duration.toHumanString() Add a function for `Duration` to render itself to a human readable string. This can be used in dashboards or other situations where Durations need to be represented. --- packages/@aws-cdk/core/lib/duration.ts | 59 +++++++++++++++----- packages/@aws-cdk/core/test/test.duration.ts | 21 ++++++- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/core/lib/duration.ts b/packages/@aws-cdk/core/lib/duration.ts index 8801d6a771a1a..edf56ed3a1731 100644 --- a/packages/@aws-cdk/core/lib/duration.ts +++ b/packages/@aws-cdk/core/lib/duration.ts @@ -64,11 +64,11 @@ export class Duration { if (!days && !hours && !minutes && !seconds) { throw new Error(`Not a valid ISO duration: ${duration}`); } - return Duration.seconds( - _toInt(seconds) - + (_toInt(minutes) * TimeUnit.Minutes.inSeconds) - + (_toInt(hours) * TimeUnit.Hours.inSeconds) - + (_toInt(days) * TimeUnit.Days.inSeconds) + return Duration.millis( + _toInt(seconds) * TimeUnit.Seconds.inMillis + + (_toInt(minutes) * TimeUnit.Minutes.inMillis) + + (_toInt(hours) * TimeUnit.Hours.inMillis) + + (_toInt(days) * TimeUnit.Days.inMillis) ); function _toInt(str: string): number { @@ -143,6 +143,36 @@ export class Duration { } } + /** + * Turn this duration into a human-readable string + */ + public toHumanString(): string { + if (this.amount === 0) { return fmtUnit(0, this.unit); } + if (Token.isUnresolved(this.amount)) { return ` ${this.unit.label}`; } + + let millis = convert(this.amount, this.unit, TimeUnit.Milliseconds, { integral: false }); + const parts = new Array(); + + for (const unit of [TimeUnit.Days, TimeUnit.Hours, TimeUnit.Hours, TimeUnit.Minutes, TimeUnit.Seconds, TimeUnit.Milliseconds]) { + const wholeCount = Math.floor(convert(millis, TimeUnit.Milliseconds, unit, { integral: false })); + if (wholeCount > 0) { + parts.push(fmtUnit(wholeCount, unit)); + millis -= wholeCount * unit.inMillis; + } + } + + // 2 significant parts, that's totally enough for humans + return parts.slice(0, 2).join(' '); + + function fmtUnit(amount: number, unit: TimeUnit) { + if (amount === 1) { + // All of the labels end in 's' + return `${amount} ${unit.label.substring(0, unit.label.length - 1)}`; + } + return `${amount} ${unit.label}`; + } + } + /** * Returns a string representation of this `Duration` that is also a Token that cannot be successfully resolved. This * protects users against inadvertently stringifying a `Duration` object, when they should have called one of the @@ -183,13 +213,16 @@ export interface TimeConversionOptions { } class TimeUnit { - public static readonly Milliseconds = new TimeUnit('millis', 0.001); - public static readonly Seconds = new TimeUnit('seconds', 1); - public static readonly Minutes = new TimeUnit('minutes', 60); - public static readonly Hours = new TimeUnit('hours', 3_600); - public static readonly Days = new TimeUnit('days', 86_400); + public static readonly Milliseconds = new TimeUnit('millis', 1); + public static readonly Seconds = new TimeUnit('seconds', 1_000); + public static readonly Minutes = new TimeUnit('minutes', 60_000); + public static readonly Hours = new TimeUnit('hours', 3_600_000); + public static readonly Days = new TimeUnit('days', 86_400_000); - private constructor(public readonly label: string, public readonly inSeconds: number) { + private constructor(public readonly label: string, public readonly inMillis: number) { + // MAX_SAFE_INTEGER is 2^53, so by representing our duration in millis (the lowest + // common unit) the highest duration we can represent is + // 2^53 / 86*10^6 ~= 104 * 10^6 days (about 100 million days). } public toString() { @@ -198,8 +231,8 @@ class TimeUnit { } function convert(amount: number, fromUnit: TimeUnit, toUnit: TimeUnit, { integral = true }: TimeConversionOptions) { - if (fromUnit.inSeconds === toUnit.inSeconds) { return amount; } - const multiplier = fromUnit.inSeconds / toUnit.inSeconds; + if (fromUnit.inMillis === toUnit.inMillis) { return amount; } + const multiplier = fromUnit.inMillis / toUnit.inMillis; if (Token.isUnresolved(amount)) { throw new Error(`Unable to perform time unit conversion on un-resolved token ${amount}.`); diff --git a/packages/@aws-cdk/core/test/test.duration.ts b/packages/@aws-cdk/core/test/test.duration.ts index 41a7a6cc110c7..cc32265417709 100644 --- a/packages/@aws-cdk/core/test/test.duration.ts +++ b/packages/@aws-cdk/core/test/test.duration.ts @@ -1,5 +1,5 @@ import * as nodeunit from 'nodeunit'; -import { Duration, Stack, Token } from '../lib'; +import { Duration, Lazy, Stack, Token } from '../lib'; export = nodeunit.testCase({ 'negative amount'(test: nodeunit.Test) { @@ -107,7 +107,24 @@ export = nodeunit.testCase({ test.equal(Duration.parse('PT1D1H1M1S').toSeconds(), 1 + 60 * (1 + 60 * (1 + 24))); test.done(); - } + }, + + 'to human string'(test: nodeunit.Test) { + test.equal(Duration.minutes(0).toHumanString(), '0 minutes'); + test.equal(Duration.minutes(Lazy.numberValue({ produce: () => 5 })).toHumanString(), ' minutes'); + + test.equal(Duration.minutes(10).toHumanString(), '10 minutes'); + test.equal(Duration.minutes(1).toHumanString(), '1 minute'); + + test.equal(Duration.minutes(62).toHumanString(), '1 hour 2 minutes'); + + test.equal(Duration.seconds(3666).toHumanString(), '1 hour 1 minute'); + + test.equal(Duration.millis(3000).toHumanString(), '3 seconds'); + test.equal(Duration.millis(3666).toHumanString(), '3 seconds 666 millis'); + + test.done(); + }, }); function floatEqual(test: nodeunit.Test, actual: number, expected: number) {