Created
November 12, 2020 12:22
-
-
Save bennadel/b3bde4bef0b04652fd4704733aa9626b to your computer and use it in GitHub Desktop.
Building A Moment-Inspired .fromNow() Date Formatting Method In Angular 10.2.3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div *ngFor="let demo of demos" class="demo"> | |
<div class="demo__slider slider"> | |
<div class="slider__label"> | |
{{ demo.minLabel }} | |
</div> | |
<div class="slider__range"> | |
<input | |
#rangeRef | |
type="range" | |
[min]="demo.min" | |
[max]="demo.max" | |
[value]="demo.value" | |
(input)="updateFromNow( demo, rangeRef.value )" | |
class="slider__input" | |
/> | |
</div> | |
<div class="slider__label"> | |
{{ demo.maxLabel }} | |
</div> | |
</div> | |
<p class="demo__label"> | |
{{ demo.fromNow }} | |
</p> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
// Import the application components and services. | |
import { DateHelper } from "./date-helper"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
interface Demo { | |
min: number; | |
minLabel: string; | |
max: number; | |
maxLabel: string; | |
value: number; | |
fromNow: string; | |
} | |
@Component({ | |
selector: "app-root", | |
styleUrls: [ "./app.component.less" ], | |
templateUrl: "./app.component.html" | |
}) | |
export class AppComponent { | |
public demos: Demo[]; | |
private dateHelper: DateHelper; | |
// I initialize the app component. | |
constructor( dateHelper: DateHelper ) { | |
this.dateHelper = dateHelper; | |
var now = Date.now(); | |
var yearsAgo = new Date( now - ( 1000 * 60 * 60 * 24 * 365 * 10 ) ); | |
var yearAgo = new Date( now - ( 1000 * 60 * 60 * 24 * 365 ) ); | |
var dayAgo = new Date( now - ( 1000 * 60 * 60 * 24 ) ); | |
var hourAgo = new Date( now - ( 1000 * 60 * 60 ) ); | |
// Since the Moment.js .fromNow() method is all about relative date-times, let's | |
// create several demos with increasingly small time-spans. This way, we can more | |
// easily see how the range-input affects the output. | |
this.demos = [ | |
{ | |
min: yearsAgo.getTime(), | |
minLabel: this.dateHelper.formatDate( yearsAgo, "yyyy-MM-dd" ), | |
max: now, | |
maxLabel: "Now", | |
value: now, | |
fromNow: this.dateHelper.fromNow( now ) | |
}, | |
{ | |
min: yearAgo.getTime(), | |
minLabel: this.dateHelper.formatDate( yearAgo, "yyyy-MM-dd" ), | |
max: now, | |
maxLabel: "Now", | |
value: now, | |
fromNow: this.dateHelper.fromNow( now ) | |
}, | |
{ | |
min: dayAgo.getTime(), | |
minLabel: this.dateHelper.formatDate( dayAgo, "yyyy-MM-dd" ), | |
max: now, | |
maxLabel: "Now", | |
value: now, | |
fromNow: this.dateHelper.fromNow( now ) | |
}, | |
{ | |
min: hourAgo.getTime(), | |
minLabel: this.dateHelper.formatDate( hourAgo, "HH:mm:ss" ), | |
max: now, | |
maxLabel: "Now", | |
value: now, | |
fromNow: this.dateHelper.fromNow( now ) | |
} | |
]; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I update the given demo to use the given tick-count. | |
public updateFromNow( demo: Demo, value: string ) : void { | |
demo.value = +value; | |
demo.fromNow = this.dateHelper.fromNow( demo.value ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { formatDate as ngFormatDate } from "@angular/common"; | |
import { Inject } from "@angular/core"; | |
import { Injectable } from "@angular/core"; | |
import { LOCALE_ID } from "@angular/core"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
// CAUTION: Numbers are implicitly assumed to be milliseconds since epoch and strings are | |
// implicitly assumed to be valid for the Date() constructor. | |
type DateInput = Date | number | string; | |
var MS_SECOND = 1000; | |
var MS_MINUTE = ( MS_SECOND * 60 ); | |
var MS_HOUR = ( MS_MINUTE * 60 ); | |
var MS_DAY = ( MS_HOUR * 24 ); | |
var MS_MONTH = ( MS_DAY * 30 ); // Rough estimate. | |
var MS_YEAR = ( MS_DAY * 365 ); // Rough estimate. | |
// The Moment.js library documents the "buckets" into which the "FROM NOW" deltas fall. | |
// To mimic this logic using milliseconds since epoch, let's calculate rough estimates of | |
// all the offsets. Then, we simply need to find the lowest matching bucket. | |
// -- | |
// https://momentjs.com/docs/#/displaying/fromnow/ | |
// 0 to 44 seconds --> a few seconds ago | |
// 45 to 89 seconds --> a minute ago | |
// 90 seconds to 44 minutes --> 2 minutes ago ... 44 minutes ago | |
// 45 to 89 minutes --> an hour ago | |
// 90 minutes to 21 hours --> 2 hours ago ... 21 hours ago | |
// 22 to 35 hours --> a day ago | |
// 36 hours to 25 days --> 2 days ago ... 25 days ago | |
// 26 to 45 days --> a month ago | |
// 45 to 319 days --> 2 months ago ... 10 months ago | |
// 320 to 547 days (1.5 years) --> a year ago | |
// 548 days+ --> 2 years ago ... 20 years ago | |
// -- | |
// Here are the bucket delimiters in milliseconds: | |
var FROM_NOW_JUST_NOW = ( MS_SECOND * 44 ); | |
var FROM_NOW_MINUTE = ( MS_SECOND * 89 ); | |
var FROM_NOW_MINUTES = ( MS_MINUTE * 44 ); | |
var FROM_NOW_HOUR = ( MS_MINUTE * 89 ); | |
var FROM_NOW_HOURS = ( MS_HOUR * 21 ); | |
var FROM_NOW_DAY = ( MS_HOUR * 35 ); | |
var FROM_NOW_DAYS = ( MS_DAY * 25 ); | |
var FROM_NOW_MONTH = ( MS_DAY * 45 ); | |
var FROM_NOW_MONTHS = ( MS_DAY * 319 ); | |
var FROM_NOW_YEAR = ( MS_DAY * 547 ); | |
@Injectable({ | |
providedIn: "root" | |
}) | |
export class DateHelper { | |
private localID: string; | |
// I initialize the date-helper with the given localization token. | |
constructor( @Inject( LOCALE_ID ) localID: string ) { | |
this.localID = localID; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I return a human-friendly, relative date-string for the given input. This is | |
// intended to mimic the .fromNow() method in Moment.js: | |
public fromNow( value: DateInput ) : string { | |
var nowTick = this.getTickCount(); | |
var valueTick = this.getTickCount( value ); | |
var delta = ( nowTick - valueTick ); | |
// NOTE: We are using Math.ceil() in the following calculations so that we never | |
// round-down to a "singular" number that may clash with a plural identifier (ex, | |
// "days"). All singular numbers are handled by explicit delta-buckets. | |
if ( delta <= FROM_NOW_JUST_NOW ) { | |
return( "a few seconds ago" ); | |
} else if ( delta <= FROM_NOW_MINUTE ) { | |
return( "a minute ago" ); | |
} else if ( delta <= FROM_NOW_MINUTES ) { | |
return( Math.ceil( delta / MS_MINUTE ) + " minutes ago" ); | |
} else if ( delta <= FROM_NOW_HOUR ) { | |
return( "an hour ago" ); | |
} else if ( delta <= FROM_NOW_HOURS ) { | |
return( Math.ceil( delta / MS_HOUR ) + " hours ago" ); | |
} else if ( delta <= FROM_NOW_DAY ) { | |
return( "a day ago" ); | |
} else if ( delta <= FROM_NOW_DAYS ) { | |
return( Math.ceil( delta / MS_DAY ) + " days ago" ); | |
} else if ( delta <= FROM_NOW_MONTH ) { | |
return( "a month ago" ); | |
} else if ( delta <= FROM_NOW_MONTHS ) { | |
return( Math.ceil( delta / MS_MONTH ) + " months ago" ); | |
} else if ( delta <= FROM_NOW_YEAR ) { | |
return( "a year ago" ); | |
} else { | |
return( Math.ceil( delta / MS_YEAR ) + " years ago" ); | |
} | |
} | |
// I proxy the native formatDate() function with a partial application of the | |
// LOCALE_ID that is being used in the application. | |
public formatDate( value: DateInput, mask: string ) : string { | |
return( ngFormatDate( value, mask, this.localID ) ); | |
} | |
// --- | |
// PRIVATE METHODS. | |
// --- | |
// I return the milliseconds since epoch for the given value. | |
private getTickCount( value: DateInput = Date.now() ) : number { | |
// If the passed-in value is a number, we're going to assume it's already a | |
// tick-count value (milliseconds since epoch). | |
if ( typeof( value ) === "number" ) { | |
return( value ); | |
} | |
return( new Date( value ).getTime() ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var MS_SECOND = 1000; | |
var MS_MINUTE = ( MS_SECOND * 60 ); | |
var MS_HOUR = ( MS_MINUTE * 60 ); | |
var MS_DAY = ( MS_HOUR * 24 ); | |
var MS_MONTH = ( MS_DAY * 30 ); // Rough estimate. | |
var MS_YEAR = ( MS_DAY * 365 ); // Rough estimate. | |
var FROM_NOW_JUST_NOW = ( MS_SECOND * 44 ); | |
var FROM_NOW_MINUTE = ( MS_SECOND * 89 ); | |
var FROM_NOW_MINUTES = ( MS_MINUTE * 44 ); | |
var FROM_NOW_HOUR = ( MS_MINUTE * 89 ); | |
var FROM_NOW_HOURS = ( MS_HOUR * 21 ); | |
var FROM_NOW_DAY = ( MS_HOUR * 35 ); | |
var FROM_NOW_DAYS = ( MS_DAY * 25 ); | |
var FROM_NOW_MONTH = ( MS_DAY * 45 ); | |
var FROM_NOW_MONTHS = ( MS_DAY * 319 ); | |
var FROM_NOW_YEAR = ( MS_DAY * 547 ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment