Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (Nov. 2009) with: Ruslan Sivak
Ben Nadel at the New York ColdFusion User Group (Nov. 2009) with: Ruslan Sivak

Calculating Various Time-Deltas Between Two Dates In Angular 9.0.0-next.4

By Ben Nadel on

At work, I write a lot of Root Cause Analysis (RCA) documents (see Incident Commander). And, at the very top of each RCA document, I have to include the duration of the Incident in terms of hours and minutes. This means that I have to take two Date/Time-stamps - Start and End - and perform maths on them in my brain. This is very challenging for me, given the fact that I am just an unfrozen caveman lawyer. So, instead of continuing to count quietly on my fingers, I wanted to sit down a create a time-delta calculation tool that would do it for me in Angular 9.0.0-next.4.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Regardless of how long an Incident lasts, the duration is always in the format of h:mm. So, if an Incident started on 2019-08-01 00:00:00 AM and was resolved on 2019-08-09 01:02:03 AM, I have to calculate that the incident lasted 193 hours and 2 minutes; or, as reported in the RCA, 193:02.

To make this easy-peasy, I created an Angular app that has two inputs: one for "From" and one for "To". Both of these take a date/time String; and, when both are populated, the Angular app runs a variety of time-delta calculations which it then outputs to the page:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<div class="controls">
			<input
				#fromInput
				type="text"
				[value]="defaultFrom"
				(input)="parseDates( fromInput.value, toInput.value )"
				placeholder="From:"
				class="input"
			/>

			<span class="separator">
				&mdash;
			</span>

			<input
				#toInput
				type="text"
				[value]="defaultTo"
				(input)="parseDates( fromInput.value, toInput.value )"
				placeholder="To:"
				class="input"
			/>
		</div>

		<ul *ngIf="deltas.length" class="deltas">
			<li *ngFor="let delta of deltas" class="delta">
				{{ delta }}
			</li>
		</ul>
	`
})
export class AppComponent {

	public defaultFrom: string;
	public defaultTo: string;
	public deltas: string[];

	// I initialize the app component.
	constructor() {

		this.defaultFrom = "2019-08-01 00:00:00 AM";
		this.defaultTo = "2019-08-09 01:02:03 AM";
		this.deltas = [];

		this.parseDates( this.defaultFrom, this.defaultTo );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I parse the given dates and calculate all of the meaningful deltas.
	public parseDates( fromValue: string, toValue: string ) : void {

		var fromMs = Date.parse( fromValue );
		var toMs = Date.parse( toValue );

		// Ensure that we have a valid date-range to work with.
		if ( isNaN( fromMs ) || isNaN( toMs ) || ( fromMs > toMs ) ) {

			console.group( "Invalid date range - no calculations to perform." );
			console.log( "From:", fromMs );
			console.log( "To:", toMs );
			console.groupEnd();
			return;

		}

		var deltaSeconds = ( ( toMs - fromMs ) / 1000 );

		this.deltas = [
			this.format( this.calculateSeconds( deltaSeconds ) ),
			this.format( this.calculateMinutesSeconds( deltaSeconds ) ),
			this.format( this.calculateHoursMinutesSeconds( deltaSeconds ) ),
			this.format( this.calculateDaysHoursMinutesSeconds( deltaSeconds ) ),
			this.format( this.calculateWeeksDaysHoursMinutesSeconds( deltaSeconds ) )
		];

		// Strip out any deltas that start with "0". These won't add any additional
		// insight above and beyond the previous delta calculations.
		// --
		// NOTE: Always using the first value, even if "0 Seconds".
		this.deltas = this.deltas.filter(
			( value, index ) => {

				return( ! index || ! value.startsWith( "0" ) );

			}
		);

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I calculate the delta breakdown using Day as the largest unit.
	private calculateDaysHoursMinutesSeconds( delta: number ) : number[] {

		var days = Math.floor( delta / 60 / 60 / 24 );
		var remainder = ( delta - ( days * 60 * 60 * 24 ) );

		return( [ days, ...this.calculateHoursMinutesSeconds( remainder ) ] );

	}


	// I calculate the delta breakdown using Hour as the largest unit.
	private calculateHoursMinutesSeconds( delta: number ) : number[] {

		var hours = Math.floor( delta / 60 / 60 );
		var remainder = ( delta - ( hours * 60 * 60 ) );

		return( [ hours, ...this.calculateMinutesSeconds( remainder ) ] );

	}


	// I calculate the delta breakdown using Minute as the largest unit.
	private calculateMinutesSeconds( delta: number ) : number[] {

		var minutes = Math.floor( delta / 60 );
		var remainder = ( delta - ( minutes * 60 ) );

		return( [ minutes, ...this.calculateSeconds( remainder ) ] );

	}


	// I calculate the delta breakdown using Second as the largest unit.
	private calculateSeconds( delta: number ) : number[] {

		return( [ delta ] );

	}


	// I calculate the delta breakdown using Week as the largest unit.
	private calculateWeeksDaysHoursMinutesSeconds( delta: number ) : number[] {

		var weeks = Math.floor( delta / 60 / 60 / 24 / 7 );
		var remainder = ( delta - ( weeks * 60 * 60 * 24 * 7 ) );

		return( [ weeks, ...this.calculateDaysHoursMinutesSeconds( remainder ) ] );

	}


	// I format the set of calculated delta-values as a human readable string.
	private format( values: number[] ) : string {

		var units: string[] = [ "Weeks", "Days", "Hours", "Minutes", "Seconds" ];
		var parts: string[] = [];

		// Since the values are calculated largest to smallest, let's iterate over them
		// backwards so that we know which values line up with which units.
		for ( var value of values.slice().reverse() ) {

			parts.unshift( value.toLocaleString() + " " + units.pop() );

		}

		return( parts.join( ", " ) );

	}

}

This was a lot of fun to write because the calculations ended-up being a little bit "recursive". Not in the traditional sense; but, in the fact that each duration iteratively builds on the one before it, until it "recurses" down to Seconds, at which point it stops. So, not technically recursion; but, definitely recursiony.

Now, if we run this Angular 9 application in the browser and leave in the default date/time-stamps, we get the following output:

Various time-detals calculated from two date/time-stamps in Angular 9.0.0-next.4.

Boom! 193 hours and 2 minutes. No more having to do tedious date/time math in my brain. I love Angular and TypeScript. Building this kind of stuff is just joyful.



Reader Comments

This was great for getting my head back into Angular gear!
Like the use of the Spread operator!

I would call this 'waterfall' or 'cascade' pseudo-recursion:)

Reply to this Comment

@Charles,

Yeah, I like the idea of "cascading" calls. Angular is fun stuff! Though, I'm currently working on a proof-of-concept for something, and it's very humbling to start from scratch. I've been working on the same app for so long, I've "detrained" the muscles that think about app organization. Trying not to get discouraged !

Reply to this Comment

Yes. Sometimes, it's a bit of a shock, stepping out of our comfort zone. I have just finished a long 4 year project and I could literally pinpoint any variable, by memory, out of 100K+ lines of code.

I am now starting something new, and it is really tough changing my mindset into architecture mode. It is an FW1 project, which eases the pain somewhat!

Essentially, it is a blogging project and I have made the big decision to use a humble old textarea as the main blog article editing tool. I am going to use MarkDown, rather than use a WYSIWYG Editor like TinyMCE/CKEditor. I will add a preview button, which will make an Ajax call. The server will use Flexmark to format the MarkDown and send back the preview to the client. I am sure there is probably a JavaScript MarkDown library, but I really love the Flexmark library.

I think MarkDown is great because it limits the author's formatting options, which can be a bit overwhelming, when using something like TinyMCE. Like we were discussing about your Breadboarding tool, I am hoping it will allow the author to concentrate on the content rather than on which formatting option to use. The only part that requires some deliberation, is how to allow authors to add uploadable images to the content. I think I might use a simple file input linked to a title text input and then ask authors to add a placeholder in the content, like:

{{ image_title }}

I am going to use Google Material Light which is the vanilla version of Angular Material. This means I can nicely style the text area.

Anyway, I am waffling on a bit here, but I sympathise with your current state of mind.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.