You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2022/08/23 10:31:29 UTC

[brooklyn-ui] branch master updated: make humanized time output more precise

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git


The following commit(s) were added to refs/heads/master by this push:
     new d6ebe011 make humanized time output more precise
d6ebe011 is described below

commit d6ebe01173ca8e526ade8cb79fc3e68e2b7d9550
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Tue Aug 23 11:31:06 2022 +0100

    make humanized time output more precise
---
 .../components/task-list/task-list.directive.js    |  13 +-
 .../task-sunburst/task-sunburst.directive.js       |   8 +-
 ui-modules/app-inspector/package.json              |   1 -
 ui-modules/utils/package.json                      |   1 +
 ui-modules/utils/utils/general.js                  |  65 ++++++++++
 ui-modules/utils/utils/momentp.js                  | 140 +++++++++++++++++++++
 ui-modules/utils/utils/momentp.spec.js             |  67 ++++++++++
 7 files changed, 280 insertions(+), 15 deletions(-)

diff --git a/ui-modules/app-inspector/app/components/task-list/task-list.directive.js b/ui-modules/app-inspector/app/components/task-list/task-list.directive.js
index 7fbc74f1..457d0268 100644
--- a/ui-modules/app-inspector/app/components/task-list/task-list.directive.js
+++ b/ui-modules/app-inspector/app/components/task-list/task-list.directive.js
@@ -17,7 +17,7 @@
  * under the License.
  */
 import angular from "angular";
-import moment from "moment";
+import {fromNow, duration} from "brooklyn-ui-utils/utils/momentp";
 import template from "./task-list.template.html";
 
 const MODULE_NAME = 'inspector.task-list';
@@ -193,9 +193,7 @@ function topLevelTasks(tasks) {
 
 export function timeAgoFilter() {
     function timeAgo(input) {
-        if (input) {
-            return moment(input).fromNow();
-        }
+        return fromNow(input);
     }
 
     timeAgo.$stateful = true;
@@ -204,12 +202,7 @@ export function timeAgoFilter() {
 }
 export function durationFilter() {
     return function (input) {
-        if (angular.isNumber(input)) {
-            if (input==0) { return "a fraction of a millisecond"; }
-            if (input<100) { return "a few milliseconds"; }
-            if (input<1000) { return "a fraction of a second"; }
-            return moment.duration(input).humanize();
-        }
+        return duration(input);
     }
 }
 
diff --git a/ui-modules/app-inspector/app/components/task-sunburst/task-sunburst.directive.js b/ui-modules/app-inspector/app/components/task-sunburst/task-sunburst.directive.js
index 3380dead..6a9899cb 100644
--- a/ui-modules/app-inspector/app/components/task-sunburst/task-sunburst.directive.js
+++ b/ui-modules/app-inspector/app/components/task-sunburst/task-sunburst.directive.js
@@ -17,7 +17,7 @@
  * under the License.
  */
 import angular from "angular";
-import moment from "moment";
+import {fromNow, duration} from "brooklyn-ui-utils/utils/momentp";
 import * as d3 from "d3";
 import * as util from "./task-sunburst.util";
 import template from "./task-sunburst.template.html";
@@ -236,10 +236,10 @@ function initVisualization($scope, $element, $state) {
             detail3 = 
               (t.isError ? "Error running task. " : "")+
               "Completed "+
-              (moment(t.endTimeUtc).fromNow())+"; "+
-              "took "+moment.duration(t.endTimeUtc - t.startTimeUtc).humanize()+". ";
+              (fromNow(t.endTimeUtc))+"; "+
+              "took "+duration(t.endTimeUtc - t.startTimeUtc)+". ";
         } else if (t.startTimeUtc) {
-            detail3 = "In progress. Started "+(moment(t.startTimeUtc).fromNow())+".";
+            detail3 = "In progress. Started "+(fromNow(t.startTimeUtc))+".";
         } else {
             detail3 = "Not started.";
         }
diff --git a/ui-modules/app-inspector/package.json b/ui-modules/app-inspector/package.json
index 5bb2f452..006fd47f 100644
--- a/ui-modules/app-inspector/package.json
+++ b/ui-modules/app-inspector/package.json
@@ -55,7 +55,6 @@
     "font-awesome": "^4.6.3",
     "isomorphic-fetch": "^2.2.1",
     "lodash": "^4.16.4",
-    "moment": "^2.15.1",
     "ngclipboard": "^1.1.1",
     "normalizr": "^2.2.1"
   },
diff --git a/ui-modules/utils/package.json b/ui-modules/utils/package.json
index f3c59c15..66df2ac8 100644
--- a/ui-modules/utils/package.json
+++ b/ui-modules/utils/package.json
@@ -46,6 +46,7 @@
     "jssha": "^2.2.0",
     "lodash": "^4.15.0",
     "marked": "^2.0.1",
+    "moment": "^2.15.1",
     "query-string": "^7.0.1",
     "rxjs": "^5.0.0-beta.11"
   },
diff --git a/ui-modules/utils/utils/general.js b/ui-modules/utils/utils/general.js
index cff70a0e..719877e3 100644
--- a/ui-modules/utils/utils/general.js
+++ b/ui-modules/utils/utils/general.js
@@ -76,3 +76,68 @@ export function capitalizeFilter() {
         return capitalize(input);
     }
 }
+
+const TOLERANCE = 0.0000000001;
+
+export function isEqualWithinTolerance(n, n2, tolerance) {
+    return Math.abs(n2 - n)<(tolerance||TOLERANCE);
+}
+
+export function isInteger(n) {
+    return isEqualWithinTolerance(n, Math.round(n));
+}
+
+/** returns number rounded as first arg, and actual number of decimal places populated as second */
+function roundNumericWithPlaces(n, maxDecimalDigits, minDecimalDigits, onlyCountSignificantDecimalDigits, countNines) {
+    maxDecimalDigits = maxDecimalDigits || 0;
+    minDecimalDigits = minDecimalDigits || 0;
+    if (countNines) n = 1-n;
+
+    // one recommended way to round; but seems inefficient using strings, causes round(0.499, 2) to show 0.5 not 0.50, and doesn't deal with significant decimal digits
+    // return Number(Math.round(Number(''+n+'e'+maxDecimalDigits))+'e-'+maxDecimalDigits);
+
+    let placesToShow = 0;
+    let significantPlaces = 0;
+    for (;;) {
+        if (isInteger(n)) break;
+        n *= 10;
+        if (onlyCountSignificantDecimalDigits && !significantPlaces) {
+            if (isEqualWithinTolerance(n, 0, 1)) {
+                // accept an extra digit if we are still dealing with insignificant digits
+                significantPlaces--;
+            }
+        }
+        significantPlaces++;
+        placesToShow++;
+        if (significantPlaces >= maxDecimalDigits && significantPlaces>0 && placesToShow>=minDecimalDigits) break;
+    }
+    let nr = Math.round(n);
+    if (!maxDecimalDigits && placesToShow > significantPlaces) {
+        // if no decimal digits but significant places then keep the right number of zeroes/ones
+        nr = Math.round(nr/10);
+        placesToShow--;
+    }
+    let i = placesToShow;
+    while (i-->0) nr/=10;
+    if (countNines) nr = 1-nr;
+    return [nr, Math.max(placesToShow, minDecimalDigits)];
+}
+
+/** rounds up to a given number of places after the decimal point;
+ * but unlike Number.toFixed if the number is exact, it does not create needless trailing zeros.
+ * so eg round(0.501, 2) will give 0.50 but round(0.50, 2) will give 0.5.
+ *
+ * optionally only counts significant digits, which ignores leading zeroes, so eg
+ * whereas round(0.00123, 2) would give 0.00, round(0.00123, 2, true) would give 0.0012.
+ * (this is especially useful when rounding nines).
+ */
+export function round(n, maxDecimalDigits, onlyCountSignificantDecimalDigits) {
+    const [number, places] = roundNumericWithPlaces(n, maxDecimalDigits, 0, onlyCountSignificantDecimalDigits, false);
+    return number;
+}
+
+/** as round, but returning a string */
+export function rounded(n, maxDecimalDigits, onlyCountSignificantDecimalDigits) {
+    const [number, places] = roundNumericWithPlaces(n, maxDecimalDigits, 0, onlyCountSignificantDecimalDigits, false);
+    return number.toFixed(places);
+}
diff --git a/ui-modules/utils/utils/momentp.js b/ui-modules/utils/utils/momentp.js
new file mode 100644
index 00000000..523dcdae
--- /dev/null
+++ b/ui-modules/utils/utils/momentp.js
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* momentp is like moment.js, but returns more precise humanized output, e.g. '1m 20s ago` or `1y 220d` */
+
+import {capitalize, rounded} from "./general";
+
+export class MomentPrecise {
+    precisionForFromNow = 5000;
+    summaryForBelowPrecisionThreshhold = "a few seconds";
+    capitalized = false;
+
+    constructor() {
+    }
+
+    setPrecisionForFromNow(precisionMillis, summaryForBelowPrecisionThreshhold) {
+        this.precisionForFromNow = precisionMillis;
+        this.summaryForBelowPrecisionThreshhold = summaryForBelowPrecisionThreshhold;
+        return this;
+    }
+
+    setCapitalized(capitalized) {
+        this.capitalized = capitalized;
+        return this;
+    }
+
+    capitalize(s) {
+        if (this.capitalized) return capitalize(s);
+        return s;
+    }
+
+    fromNow(utc) {
+        if (!utc) return this.capitalize("-");
+        var millis = utc - Date.now();
+        if (millis==0) return this.capitalize("now");
+        const ago = millis < 0;
+        if (ago) millis = -millis;
+
+        var s;
+        if (millis*4 < 3*this.precisionForFromNow) s = this.summaryForBelowPrecisionThreshhold;
+        else if (this.precisionForFromNow>0) s = this.duration( Math.round(millis/this.precisionForFromNow)*this.precisionForFromNow );
+        else s = this.duration(millis);
+
+        if (ago) s = s +" ago";
+        else s = "in " + s;
+        return this.capitalize(s);
+    }
+
+    duration(millis) {
+        if (millis === 0) return this.capitalize("0ms");
+        if (!millis) return this.capitalize("-");
+        let tweak = x=>this.capitalize(x);
+        if (millis < 0) {
+            millis = -millis;
+            tweak = x=> {
+                return this.capitalized("-" + x.replace(/ /g, ""));
+            }
+        }
+
+        if (millis < 1000) return tweak(millis+"ms");
+
+        let secs = millis/1000;
+        let secsR = Math.round(secs);
+        if (secsR < 10) return tweak(rounded(secs, 1)+"s");
+        if (secsR < 60) return tweak(secsR+"s");
+
+        let mins = Math.floor(secs/60);
+        let minsR = Math.round(secs/60);
+        secs = Math.round(secs - mins*60);
+        if (secs>=60) {
+            mins++;
+            secs -= 60;
+        }
+        if (mins < 5) {
+            return tweak(mins +"m" + " " + secs +"s");
+        }
+        if (minsR < 60) {
+            return tweak(minsR) +" mins";
+        }
+
+        let hours = Math.floor(minsR/60);
+        let hoursR = Math.round(minsR/60);
+        mins = Math.round(mins - hours*60);
+        if (mins >= 60) {
+            hours++;
+            mins -= 60;
+        }
+        if (hours < 4) return tweak(hours +"h" +" " + mins +"m");
+        if (hoursR < 24) return tweak(hoursR) +" hours";
+
+        let days = Math.floor(hoursR/24);
+        let daysR = Math.round(hoursR/24);
+        hours = Math.round(hours - days*24);
+        if (hours >= 24) {
+            days++;
+            hours -= 24;
+        }
+        if (days < 7) return tweak(days + "d" +" " + hours +"h");
+        if (daysR < 365) return tweak(daysR) + " days";
+
+        let years = Math.floor(daysR / 365.25);
+        let yearsR = Math.round(daysR / 365.25);
+        days = Math.round(days - years*365.25);
+        if (days >= 365) {
+            years += 1;
+            days -= 365;
+        }
+        if (years<=0) {
+            years = 1;
+            days = 0;
+        }
+        if (years < 10) return tweak(years + "y" +" " + days +"d");
+        return tweak(yearsR) +" years";
+    }
+
+}
+
+export function fromNow(utc) {
+    return new MomentPrecise().fromNow(utc);
+}
+
+export function duration(utc) {
+    return new MomentPrecise().duration(utc);
+}
diff --git a/ui-modules/utils/utils/momentp.spec.js b/ui-modules/utils/utils/momentp.spec.js
new file mode 100644
index 00000000..4fa785c9
--- /dev/null
+++ b/ui-modules/utils/utils/momentp.spec.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* momentp is like moment.js, but returns more precise humanized output */
+
+import * as mp from "./momentp";
+import moment from "moment";
+
+describe('momentp', ()=> {
+    it('should evaluate fromNow correctly', ()=> {
+        expect(moment(Date.now() + 3000).fromNow()).toBe("in a few seconds");
+        expect(moment(Date.now() - 3000).fromNow()).toBe("a few seconds ago");
+        // moment will class "12s" as "a few seconds ago"; our routines are more precise
+        expect(moment(Date.now() - 12000).fromNow()).toBe("a few seconds ago");
+
+        expect(mp.fromNow(Date.now()
+            + 3000)).toBe("in a few seconds");
+        expect(new mp.MomentPrecise().setCapitalized(true).fromNow(Date.now() + 3000)).toBe("In a few seconds");
+        expect(mp.fromNow(Date.now() - 3000)).toBe("a few seconds ago");
+        expect(mp.fromNow(Date.now() - 8000)).toBe("10s ago");
+        expect(mp.fromNow(Date.now() - 8123)).toBe("10s ago");
+        expect(mp.fromNow(Date.now() - 10123)).toBe("10s ago");
+        expect(mp.fromNow(Date.now() - 13123)).toBe("15s ago");
+        expect(mp.fromNow(Date.now() - 43123)).toBe("45s ago");
+        expect(mp.fromNow(Date.now() - 62123)).toBe("1m 0s ago");
+        expect(mp.fromNow(Date.now() - 63123)).toBe("1m 5s ago");
+    });
+
+    it('should evaluate duration correctly', ()=> {
+        expect(mp.duration(3*1000)).toBe("3s");
+        expect(mp.duration(60*1000)).toBe("1m 0s");
+        expect(mp.duration(61*1000)).toBe("1m 1s");
+        expect(mp.duration(30 * 60*1000)).toBe("30 mins");
+        expect(mp.duration(8000)).toBe("8s");
+        expect(mp.duration(8123)).toBe("8.1s");
+        expect(mp.duration(10123)).toBe("10s");
+        expect(mp.duration(59501)).toBe("1m 0s");
+        expect(mp.duration(62123)).toBe("1m 2s");
+        expect(mp.duration(20*60000 + 2123)).toBe("20 mins");
+        expect(mp.duration(62123 + 60*60*1000)).toBe("1h 1m");
+        expect(mp.duration(62123 + 8*60*60*1000)).toBe("8 hours");
+        expect(mp.duration(-1 + 24*60*60*1000)).toBe("1d 0h");
+        expect(mp.duration(62123 + 24*60*60*1000)).toBe("1d 0h");
+        expect(mp.duration(62123 + 25*60*60*1000)).toBe("1d 1h");
+        expect(mp.duration(62123 + 30*24*60*60*1000)).toBe("30 days");
+        expect(mp.duration(62123 + 360*24*60*60*1000)).toBe("360 days");
+        expect(mp.duration(62123 + 370*24*60*60*1000)).toBe("1y 5d");
+        expect(mp.duration(62123 + 20*365*24*60*60*1000)).toBe("20 years");
+    });
+
+});