@xassist/xassist-date
Version:
helper functions for date manipulation
420 lines (384 loc) • 13.1 kB
JavaScript
import { object } from "@xassist/xassist-object";
//var { object } =require("@xassist/xassist-object");
function getDecimal(num){
return+((num<0?"-.":".")+num.toString().split(".")[1])||0
}
var _durationRegexp=[
{key:"year",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?y|Y|years?|Years?)(?![a-zA-z])/g}, //years component
{key:"month",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?M|months?|Months?)(?![a-zA-z])/g}, //months component
{key:"day",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?d|D|days?|Days?)(?![a-zA-z])/g}, //days component
{key:"hour",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?h|H|hours?|Hours?)(?![a-zA-z])/g}, //hours component
{key:"minute",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?m|mins?|Mins?|minutes?|Minutes?)(?![a-zA-z])/g}, //minutes component
{key:"second",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?s|S|secs?|Secs?|seconds?|Seconds?)(?![a-zA-z])/g}, //seconds component
{key:"millisecond",re: /(-?\d*(?:[.,]\d*)?)(?:[ ]?ms|millis?|m[sS]ecs?|m[sS]econds?|milli[sS]ecs?|milli[sS]econds?)(?![a-zA-z])/g}, //milliseconds component
];
/* regexp explanation for each component eg for year
/ //start regexp
( //capturing group 1 number of years
-? //optional negative number
\d* //zero or more digits
(?: //non capturing group
[.,] //matches single character (point or ,)
\d* //zero or more digits
)? //optional could be omitted
) //capturing group finished (matches on 1.25|0.25|1000|1.|.|.5 or with a comma.)
(?: //non capturing group (years)
[ ]? //optional space
y|Y|years?|Years? //y or Y or year or years or Year or Years
) //closes group for (y||Y||year||years||Year||Years)
(?![a-zA-z]) //negative lookahead everything except a-z or A-Z
/g //global match
*/
function _parseDurationString(d,durStr){
var matchMade;
//parse string
//eg 1y1M1d1h1m1s1ms
//abbrev
for(var i=0,len=_durationRegexp.length;i<len;i++){
//for multiple matches on same regegexp we could use exec
while (matchMade = _durationRegexp[i].re.exec(durStr)) {
d[_durationRegexp[i].key]+=parseFloat((matchMade[1]||"0").replace(",","."));
}
}
return d;
}
var duration=function(){
return new XaDuration([].slice.call(arguments));
}
function XaDuration(initArray){
this.year=0;
this.month=0;
this.day=0;
this.hour=0;
this.minute=0;
this.second=0;
this.millisecond=0;
this.normalized=false;
//this.dayReserve=0; //hold converted month decimals in days, to calculate when really needed
this.init(initArray);
}
XaDuration.prototype._keyOrder=[ 'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond' ]
XaDuration.prototype.init=function(a){
if (a.length===1){
if(typeof a[0]==="string"){
_parseDurationString(this,a[0])
}
else if(typeof a[0]==="number"){
//milliseconds is default value
this.millisecond+=a[0];
}
else if(typeof a[0]==="object"){
object(this).mergeUnique(a[0]);
}
}
if(a.length>1){
a.forEach(function(val,i){
//console.log(val,i,wantedKeys[i],this[wantedKeys[i]])
if(i<this._keyOrder.length&&typeof val==="number"){
this[this._keyOrder[i]]+=val;
}
},this)
}
}
/*
we should normalize the floating values to do calculations with dates
this works for
- s=>milisecs (*1000)
- min=>sec (*60)
- hr=>min (*60)
- day=>hr (*24)
- year=>month (*12)
exception
-month=>day (*30 or *31 or *28 or even *29)
So there is a break in normalization between day and month setting apart month and year!
*/
var _conversionCoefficients={
year:{
coeff:7/(30.436875*12),
exactType:"big"
},
month:{
coeff:7/30.436875,
exactType:"big"
},
week:{
coeff:1,
exactType:"small"
},
day:{
coeff:7,
exactType:"small"
},
hour:{
coeff:168,
exactType:"small"
},
minute:{
coeff:10080,
exactType:"small"
},
second:{
coeff:604800,
exactType:"small"
},
millisecond:{
coeff:604800000,
exactType:"small"
}
}
XaDuration.prototype.normalize=function(exact){
exact=(typeof exact==="undefined"?true:!!exact);
//first we normalize up to upscale the factors thats needed like 12months becomes 1 year
this.normalizeUp(exact);
//the only factor that is decimal is the one from day to month so
//after upscaling the only attribute potentially remaining decimal is day
//now we can normalize down to eliminate decimals
if(!this.normalized){
this.normalizeDown(exact)
//this could introduce other scaling factors that should be upscaled (added hours so hours fall above 24 or lower)
//since those all fall lower then day we should only once scale up if necessary (but we put true to be sure to not change months
this.normalizeUp(true);
}
}
XaDuration.prototype.normalizeDown=function(exact){
var key,dec,nextKey,factor;
exact=(typeof exact==="undefined"?true:!!exact);
for (var i=0,len=this._keyOrder.length;i<len;i++){
key=this._keyOrder[i];
nextKey=this._keyOrder[i+1];
if(nextKey){
factor=this.getConversionFactor(key,nextKey);
if(!exact||factor.exact){
dec=getDecimal(this[key]);
this[key]=this[key]-dec;
this[nextKey]+=dec*factor.factor;
}
}
}
//month is only one that can be decimal because only conversion thaht can be skipped with exact
//we should not check millisecond because it is allowed to be decimal
this.normalized=((getDecimal(this.month))===0);
return this;
}
XaDuration.prototype.normalizeUp=function(exact){
var key,nextKey,factor,oldVal,i=this._keyOrder.length,normalized=true;
exact=(typeof exact==="undefined"?true:!!exact)
while(i--){
key=this._keyOrder[i];
nextKey=this._keyOrder[i-1];
if(nextKey){
factor=this.getConversionFactor(nextKey,key);
if(!exact||factor.exact){
oldVal=this[key];
this[key]=oldVal%factor.factor;
this[nextKey]+=(oldVal-this[key])/factor.factor;
}
}
if(i!==this._keyOrder.length-1){
normalized=normalized&&((this[key]*10%10/10)===0)
}
}
this.normalized=normalized;
return this;
}
XaDuration.prototype.getConversionFactor=function(fromUnit,toUnit){
if(_conversionCoefficients.hasOwnProperty(fromUnit)&&_conversionCoefficients.hasOwnProperty(toUnit)){
return {
factor:(_conversionCoefficients[toUnit].coeff/_conversionCoefficients[fromUnit].coeff),
exact:(_conversionCoefficients[toUnit].exactType===_conversionCoefficients[fromUnit].exactType)
}
}
else{
throw new TypeError("Invalid unit conversion type");
}
}
XaDuration.prototype.valueOf=function(){
//returns number of milliseconds
var result=0,key;
for (var i=0,len=this._keyOrder.length;i<len;i++){
key=this._keyOrder[i];
result+=(this.getConversionFactor(key,"millisecond").factor*this[key]);
}
return result
}
XaDuration.prototype.toString=function(){
var result=[],key,v=this.valueOf(),dur=duration(Math.abs(v));
dur.normalize(false)
for (var i=0,len=this._keyOrder.length;i<len;i++){
key=this._keyOrder[i];
if(dur[key]!==0){
result.push(dur[key]+" "+key+(dur[key]>1?"s":""));
}
}
if(v<0){
result.push("ago")
}
return result.join(' ')+".";
}
XaDuration.prototype.format=function(tolerance){
//tolerance is the relative tolerance that may be accepted in the string representation
//tolerance is a percentage eg 0.01=1% and should be given as a numeric value<1
//if tolerance is given as 1 just the largest component
//decimals are never given 3.5 years is represented as 3 years 6 months
var result=[],key,
v=this.valueOf(),
absV=Math.abs(v),
dur=duration(absV), //clone duration
currentVal=0,
relError=1;
if(!tolerance){
return this.toString();
}
tolerance=Math.abs(tolerance)
tolerance=tolerance>1?1:tolerance;
dur.normalize(false);
for (var i=0,len=this._keyOrder.length;i<len&&relError>=tolerance;i++){
key=this._keyOrder[i];
if(dur[key]!==0){
currentVal+=dur[key]*this.getConversionFactor(key,"millisecond").factor
result.push(dur[key]+" "+key+(dur[key]>1?"s":""));
relError=1-currentVal/absV;
}
}
//check if we could lower the relError by adding 1 to last found key (ex. rounding 3.5 years to 4)
currentVal+=1*this.getConversionFactor(key,"millisecond").factor;
if(relError>=(-1+currentVal/absV)){ //new relative error is negative because we are rounding up
result.push(result.pop().split(" ").map(function(v,i){return (i==0?+v+1:v)}).join(" "));
}
if(v<0){
result.push("ago")
}
return result.join(' ')+".";
}
XaDuration.prototype.addDuration=function(dur){
var key,i,len;
for (i=0,len=this._keyOrder.length;i<len;i++){
key=this._keyOrder[i]
if(dur.hasOwnProperty(key)&&typeof dur[key]==="number"){
this[key]+=dur[key];
}
}
return this;
}
XaDuration.prototype.removeIntervalOfType=function(type,value){
if(~this._keyOrder.indexOf(type) ){
value=(typeof value==="number"?value:this[type]);
this[type]-=value;
return value;
}
else{
return 0;
//throw typeError("Invalid interval type");
}
}
XaDuration.prototype.normalizeMonth=function(numberOfDays){
var dec=getDecimal(this.month);
this.month=this.month-dec;
this.day+=numberOfDays*dec;
return this.normalizeDown();
}
/*console.time('parser')
for (i=0;i<10000;i++) {
new XaDuration("14y 2milliseconds 1d 4y 13 hours 15days ");
}
console.timeEnd('parser') //takes a 50 ms for 10/000 values
console.log(new XaDuration(14.157))
var a=new XaDuration({year:157})
console.log(Object.keys(a))
console.log(new XaDuration(1,2,3,4,5,6,7,8))
console.log(new XaDuration(1,2,3,"test",5))*/
//console.log(new XaDuration("14y2milliseconds 1d 4ys 13 hours 15d")) //4ys is faulty
/*
var a=new Date(2017,2,15,0,0,10,1570);
console.log("orig date : "+a+"\thour: "+a.getHours())
var b=new Date(a)
console.log("clone date : "+b+"\thour: "+b.getHours())
b.setMonth(b.getMonth()+1);
console.log("1 month later : "+b+"\t\thour: "+b.getHours())
console.log("addmonth : "+addMonths(a,1)+"\t\thour: "+addMonths(a,1).getHours())
console.log("addmonth2 : "+addMonths2(a,1)+"\t\thour: "+addMonths2(a,1).getHours())
checkAddMonths()
logTimings(checkSpeed(100000,new Date(1999,11,31)),["addMonths","addMonths2"]);
function checkSpeed(iterations,initDate){
var res1=initDate,res2=initDate,tStart,tEnd,time1,time2,i;
tStart = performance.now();
for (i=0;i<iterations;i++){
res1=addMonths(res1,1);
}
tEnd = performance.now();
time1=tEnd-tStart;
tStart = performance.now();
for (i=0;i<iterations;i++){
res2=addMonths2(res2,1);
}
tEnd = performance.now();
time2=tEnd-tStart;
return [
{result:res1,timing:time1},
{result:res2,timing:time2}
]
}
console.log(addMonths2(new Date(),1));
console.log(addMonths2(new Date()));
console.log(addMonths2(new Date(),NaN));
function logTimings(result,names){
console.log("Timing result")
console.log("*********")
result.forEach((x,i)=>x.func=names[i])
console.table(result);
}
function checkAddMonths(){
console.log("Checking months added")
console.log("******************")
var d=[
[new Date(2015,0,1),1 ,'2015-2-1'],
[new Date(2015,0,1),2 ,'2015-3-1'],
[new Date(2015,0,1),3 ,'2015-4-1'],
[new Date(2015,0,1),4 ,'2015-5-1'],
[new Date(2015,1,15),1,'2015-3-15'],
[new Date(2015,0,31), 1, '2015-2-28'],
[new Date(2016,0,31),1, '2016-2-29'],
[new Date(2015,0,01),11,'2015-12-1'],
[new Date(2015,0,01),12,'2016-1-1'],
[new Date(2015,0,01),24,'2017-1-1'],
[new Date(2015,1,28),12,'2016-2-28'],
[new Date(2015,2,01),12,'2016-3-1'],
[new Date(2016,1,29),12,'2017-2-28']
]
for (var i=0,len=d.length;i<len;i++){
console.log("orig date\t\t\t\t: "+d[i][0]+"\t\t\tAdd Months: "+d[i][1])
console.log("addMonth\t\t\t: "+addMonths(d[i][0],d[i][1])+"\t\t\texpected result: "+d[i][2]+"\t\tLocaleString: "+addMonths(d[i][0],d[i][1]).toLocaleString().split(" ")[0])
console.log("addMonth2\t\t\t: "+addMonths2(d[i][0],d[i][1])+"\t\t\texpected result: "+d[i][2]+"\t\tLocaleString: "+addMonths2(d[i][0],d[i][1]).toLocaleString().split(" ")[0])
}
console.log("Ended")
console.log("*****")
}
function isLeapYear(year) {
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
}
function getDaysInMonth(year, month) {
return [31, (isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
}
function addMonths(date, m) {
var d = new Date(date),
n = date.getDate();
d.setDate(1);
d.setMonth(d.getMonth() + m);
d.setDate(Math.min(n, getDaysInMonth(d.getFullYear(), d.getMonth())));
return d;
}
function addMonths2(date,value){
var m, d = new Date(+date),day
if(typeof value!=="number"){
return d
}
day = d.getDate()
d.setMonth(d.getMonth() + value, 1)
m = d.getMonth()
d.setDate(day)
if (d.getMonth() !== m){
d.setDate(0)
}
return d
}*/
export default duration;