Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created November 12, 2020 12:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/b3bde4bef0b04652fd4704733aa9626b to your computer and use it in GitHub Desktop.
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
<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>
// 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 );
}
}
// 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() );
}
}
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