Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Jessica Kennedy
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Jessica Kennedy

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

By
Published in Comments (3)

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.

Want to use code from this post? Check out the license.

Reader Comments

447 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:)

15,841 Comments

@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 !

447 Comments

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel