Created
November 18, 2020 11:58
-
-
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
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
(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 ); |
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
<!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