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");
+ });
+
+});