Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created November 18, 2020 11:58
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/e82774d195d25e21d8852c608fa09955 to your computer and use it in GitHub Desktop.
Save bennadel/e82774d195d25e21d8852c608fa09955 to your computer and use it in GitHub Desktop.
Replacing All External Date Libraries With 300 Lines-Of-Code In AngularJS 1.2.22
(function( ng, app ) {
"use strict";
app.factory( "dateHelper", DateHelperFactory );
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 );
function DateHelperFactory( $filter ) {
// Return the public API.
return({
add: add,
daysInMonth: daysInMonth,
diff: diff,
format: format,
fromNow: fromNow
});
// ---
// PUBLIC METHODS.
// ---
// I add the given date/time delta to the given date. A new date is returned.
function add( part, delta, input ) {
var result = new Date( input );
switch ( part ) {
case "year":
case "y":
result.setFullYear( result.getFullYear() + delta );
break;
case "month":
case "M":
result.setMonth( result.getMonth() + delta );
break;
case "day":
case "d":
result.setDate( result.getDate() + delta );
break;
case "hour":
case "h":
result.setHours( result.getHours() + delta );
break;
case "minute":
case "m":
result.setMinutes( result.getMinutes() + delta );
break;
case "second":
case "s":
result.setSeconds( result.getSeconds() + delta );
break;
case "millisecond":
case "sss":
result.setMilliseconds( result.getMilliseconds() + delta );
break;
// NOTE: Unlike in the TypeScript version, we can't rely on the compiler
// to limit the value of "part" based on our type definitions.
default:
throw( new Error( "Unsupported date part: " + part ) );
break;
}
return( result );
}
// I return the number of days in the given month. The year must be included
// since the days in February change during a leap-year.
function daysInMonth( year, month ) {
var lastDayOfMonth = new Date(
year,
// Go to the "next" month. This is always safe to do; if the next month
// is beyond the boundary of the current year, it will automatically
// become the appropriate month of the following year.
( month + 1 ),
// Go to the "zero" day of the "next" month. Since days range from 1-31,
// the "0" day will automatically roll back to the last day of the
// previous month. And, since we did ( month + 1 ) above, it will be
// ( month + 1 - 1 ) ... or simply, the last day of the "month" in
// question.
0
);
return( lastDayOfMonth.getDate() );
}
// I determine the mount by which the first date is less than the second date
// using the given date part. Returns an INTEGER that rounds down.
// --
// CAUTION: The Year / Month / Day diff'ing is a ROUGH ESTIMATE that should be
// good enough for the vast majority of User Interface (UI) cases, especially
// since we're rounding the differences in general. If you need something more
// accurate, that would be the perfect reason to pull-in an external date
// library.
function diff( part, leftDateInput, rightDateInput ) {
var delta = ( getTickCount( rightDateInput ) - getTickCount( leftDateInput ) );
var multiplier = 1;
// We always want the delta to be a positive number so that the .floor()
// operation in the following switch truncates the value in a consistent way.
// We will compensate for the normalization by using a dynamic multiplier.
if ( delta < 0 ) {
delta = Math.abs( delta );
multiplier = -1;
}
switch ( part ) {
case "year":
case "y":
// CAUTION: Rough estimate.
return( Math.floor( delta / MS_YEAR ) * multiplier );
break;
case "month":
case "M":
// CAUTION: Rough estimate.
return( Math.floor( delta / MS_MONTH ) * multiplier );
break;
case "day":
case "d":
// CAUTION: Rough estimate.
return( Math.floor( delta / MS_DAY ) * multiplier );
break;
case "hour":
case "h":
return( Math.floor( delta / MS_HOUR ) * multiplier );
break;
case "minute":
case "m":
return( Math.floor( delta / MS_MINUTE ) * multiplier );
break;
case "second":
case "s":
return( Math.floor( delta / MS_SECOND ) * multiplier );
break;
case "millisecond":
case "sss":
return( delta * multiplier );
break;
// NOTE: Unlike in the TypeScript version, we can't rely on the compiler
// to limit the value of "part" based on our type definitions.
default:
throw( new Error( "Unsupported date part: " + part ) );
break;
}
}
// I proxy the native date $filter.
function format( value, mask ) {
return( $filter( "date" )( value, mask ) );
}
// I return a human-friendly, relative date-string for the given input. This is
// intended to mimic the .fromNow() method in Moment.js:
function fromNow( value ) {
var nowTick = getTickCount();
var valueTick = getTickCount( value );
var delta = ( nowTick - valueTick );
var prefix = "";
var infix = "";
var suffix = " ago"; // Assume past-dates by default.
// If we're dealing with a future date, we need to flip the delta so that our
// buckets can be used in a consistent manner. We will compensate for this
// change by using a different prefix/suffix.
if ( delta < 0 ) {
delta = Math.abs( delta );
prefix = "in ";
suffix = "";
}
// 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 ) {
infix = "a few seconds";
} else if ( delta <= FROM_NOW_MINUTE ) {
infix = "a minute";
} else if ( delta <= FROM_NOW_MINUTES ) {
infix = ( Math.ceil( delta / MS_MINUTE ) + " minutes" );
} else if ( delta <= FROM_NOW_HOUR ) {
infix = "an hour";
} else if ( delta <= FROM_NOW_HOURS ) {
infix = ( Math.ceil( delta / MS_HOUR ) + " hours" );
} else if ( delta <= FROM_NOW_DAY ) {
infix = "a day";
} else if ( delta <= FROM_NOW_DAYS ) {
infix = ( Math.ceil( delta / MS_DAY ) + " days" );
} else if ( delta <= FROM_NOW_MONTH ) {
infix = "a month";
} else if ( delta <= FROM_NOW_MONTHS ) {
infix = ( Math.ceil( delta / MS_MONTH ) + " months" );
} else if ( delta <= FROM_NOW_YEAR ) {
infix = "a year";
} else {
infix = ( Math.ceil( delta / MS_YEAR ) + " years" );
}
return( prefix + infix + suffix );
}
// ---
// PRIVATE METHODS.
// ---
// I return the milliseconds since epoch for the given value.
function getTickCount( value ) {
if ( value === undefined ) {
value = Date.now();
}
// 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() );
}
}
})( angular, app );
<!doctype html>
<html lang="en" ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Replacing All External Date Libraries With 300 Lines-Of-Code In AngularJS 1.2.22
</title>
<link rel="stylesheet" type="text/css" href="./demo.css">
</head>
<body ng-controller="appController">
<h1>
Replacing All External Date Libraries With 300 Lines-Of-Code In AngularJS 1.2.22
</h1>
<div class="result">
<span class="result__absolute">
{{ formattedDate }}
</span>
<span class="result__relative">
{{ relativeDate }}
</span>
</div>
<div class="slider">
<div class="slider__label">
Year
</div>
<input
type="range"
min="-100"
max="100"
ng-model="yearDelta"
class="slider__input"
/>
<div class="slider__value">
{{ yearDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Month
</div>
<input
type="range"
min="-100"
max="100"
ng-model="monthDelta"
class="slider__input"
/>
<div class="slider__value">
{{ monthDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Day
</div>
<input
type="range"
min="-100"
max="100"
ng-model="dayDelta"
class="slider__input"
/>
<div class="slider__value">
{{ dayDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Hour
</div>
<input
type="range"
min="-100"
max="100"
ng-model="hourDelta"
class="slider__input"
/>
<div class="slider__value">
{{ hourDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Minute
</div>
<input
type="range"
min="-100"
max="100"
ng-model="minuteDelta"
class="slider__input"
/>
<div class="slider__value">
{{ minuteDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Second
</div>
<input
type="range"
min="-100"
max="100"
ng-model="secondDelta"
class="slider__input"
/>
<div class="slider__value">
{{ secondDelta }}
</div>
</div>
<div class="slider">
<div class="slider__label">
Millis
</div>
<input
type="range"
min="-100"
max="100"
ng-model="millisecondDelta"
class="slider__input"
/>
<div class="slider__value">
{{ millisecondDelta }}
</div>
</div>
<!-- ---------------------------------------------------------------------------- -->
<!-- ---------------------------------------------------------------------------- -->
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
</script>
<script type="text/javascript" src="./date-helper.js"></script>
<script type="text/javascript">
app.controller( "appController", AppController );
function AppController( $scope, dateHelper ) {
$scope.baseDate = new Date();
$scope.dayDelta = 0;
$scope.hourDelta = 0;
$scope.millisecondDelta = 0;
$scope.minuteDelta = 0;
$scope.monthDelta = 0;
$scope.relativeDate = "";
$scope.secondDelta = 0;
$scope.yearDelta = 0;
$scope.dateMask = "yyyy-MM-dd HH:mm:ss.sss";
$scope.formattedDate = "";
$scope.relativeDate = "";
// I get called on every digest.
// --
// NOTE: Rather than have an explicit function that has to get called every
// time a date-delta is adjusted, we're just going to hook into the digest
// since we know that a new digest will be triggered on every (input) event.
$scope.$watch(
function evaluateExpression() {
var result = $scope.baseDate;
// The .add() function returns a NEW date each time, so we have to
// keep saving and reusing the result of each call.
// --
// CAUTION: Notice that we are casting the String value of the
// ngModel to Numbers.
result = dateHelper.add( "year", +$scope.yearDelta, result );
result = dateHelper.add( "month", +$scope.monthDelta, result );
result = dateHelper.add( "day", +$scope.dayDelta, result );
result = dateHelper.add( "hour", +$scope.hourDelta, result );
result = dateHelper.add( "minute", +$scope.minuteDelta, result );
result = dateHelper.add( "second", +$scope.secondDelta, result );
result = dateHelper.add( "millisecond", +$scope.millisecondDelta, result );
// NOTE: We have to return the TIME - not the result - or we run into
// an infinite-digest problem. We need to give Angular a simple value
// so that the new/old values become equivalent when no change has
// taken place.
return( result.getTime() );
},
function handleModelChange( newValue, oldValue ) {
var result = new Date( newValue );
$scope.formattedDate = dateHelper.format( result, $scope.dateMask );
$scope.relativeDate = dateHelper.fromNow( result );
console.group( "Angular Digest" );
console.log( "Date:", $scope.formattedDate );
console.log( "Relative Date:", $scope.relativeDate );
// Log the diff() for the given date - how much it is LESS THAN the base date.
console.log(
"Diff:",
dateHelper.diff( "year", result, $scope.baseDate ),
dateHelper.diff( "month", result, $scope.baseDate ),
dateHelper.diff( "day", result, $scope.baseDate ),
dateHelper.diff( "hour", result, $scope.baseDate ),
dateHelper.diff( "minute", result, $scope.baseDate ),
dateHelper.diff( "second", result, $scope.baseDate ),
dateHelper.diff( "millisecond", result, $scope.baseDate )
);
// Log the days in the given month.
console.log(
"Days in month:",
dateHelper.daysInMonth( result.getFullYear(), result.getMonth() )
);
console.groupEnd();
}
);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment