0

I'd like to accurately display, in this format, how old a user is. It seems leap years make the output a bit unexpected (I'm 23 whereas my output is 22.9). Is there anything I can do differently to account for this?

var t= (new Date()) - (new Date(1993,3,1));
t/= (1000*60*60*24*365);

http://codepen.io/kylebillings/pen/RaVLBM

Thanks in advance for your problem solving help!

  • 1
    To get accurate results you won't get around using a third party library like moment.js (http://momentjs.com/) – forrert Mar 23 '16 at 17:49
  • 1
    @forrert: It's rather unnecessary to include a third-party library to count age in years. The problem is the OP's algorithm (find millisecond difference and divide by average milliseconds in a non-leap year), which is inaccurate in *any* language. And it's pretty easy to fix: directly compare year, month, and day values of the two dates. – mellamokb Mar 23 '16 at 17:51
  • @mellamokb I agree. However, many of the answers in the linked question state that it's not 100% accurate ;) I guess from the question's sample code I just assumed the OP would want the result to be very accurate (fraction of years)... – forrert Mar 23 '16 at 18:17

1 Answers1

0

The issue with expressing the time between two dates is that there are a number of decisions to be made about how to deal with years, months and days of different lengths, e.g.

  1. If someone is born on 29 February, do they turn 1 on 28 February or 1 March the following year? Administrations (and people) differ in their opinions.

  2. Is 31 May plus one month 30 June or 1 July?

  3. When daylight saving ends, a period of time occurs twice. For times in that period, should the earlier or later time be used?

Whether a particular algorithm for determining years, months, days, etc. between two dates is correct or not depends on your answers to the above. If you choose to use a library for this, you should know how it handles all the above and be sure that it suits whoever is using it.

Below is a function that calculates the difference between two dates, the basic algorithm is to work out the values that must be added to the earlier date to get to the later date. Adding one month to 31 March gives 30 June, so no month roll overs. Similarly, 29 Feb plus one year is 28 Feb, not 1 Mar. There may be some quirks around daylight saving, it relies on the browser to get that right (at best it varies and in some cases is plain wrong), so avoid DST changeover periods. I have an algorithm that should fix those issued, but I haven't developed the code for it yet.

So the following may suit or not, but it handles issues in a consistent manner and shows a general algorithm that is more accurate than others I've encountered. It uses its own ISO 8601 extended format date parser to remove browser errors on that front, but it can't remove them all. Safari has issues around daylight saving changeover. This is just one way of doing the job and not necessarily definitive. There is an extensive suite of tests, I think I've covered everything but only further testing will confirm that.

The code runs in every browser back to IE 5, however to run the tests requires polyfills for .every, .forEach and .toISOString

// Ensure consistent parsing of ISO 8601 date strings. Some browsers will still parse
// ISO dates without a timezone as UTC (looking at you, Safari). Also, some may parse
// ISO dates without a timezone as local but most us UTC (inconsistent with ISO 8601)
// Certain versions of Chrome, allow out of bounds values in ISO 8601 strings.
// IE 8 and lower will not parse ISO 8601 strings at all.
//
// Date (and time) only treated as local: 2016-02-29, 2016-02-29T12:23:57
//
// Date with timezone applies the offset: 2016-02-29Z, 2016-02-29+10:00, 2016-02-29T12:23:57+10:00
function parseISO (s) {

  // Get offset if s ends in Z/z or +/-dd:dd 
  var zone = /z$/i.test(s)? ['+00:00'] : s.match(/[+-]\d\d:\d\d$/);
  
  // Trim offset from string before getting other parts
  // Otherwise valid strings like 2016T-04:30 will break (2016 with offset -04:30)
  var b = (zone? s.replace(/z$|[+-]\d\d:\d\d$/i,'') : s).split(/\D/);

  // Resolve zone to ISO 8601 sign orientation and minutes, or set to null if missing
  if (zone) {
    var sign = /^-/.test(zone[0])? -1 : 1;
    var z = zone[0].match(/\d+/g);
    zone = sign * (z[0]*60 + Number(z[1]));
  }
  
  // Create date. If zone available, use UTC and zone, otherwise use local and hope 
  // that the Date constructor gets it right for DST
  if (zone !== null) {
                            // y      m                  d        h         m                s        ms
    return new Date(Date.UTC(b[0], (b[1]? b[1]-1 : 0), b[2]||1, b[3]||0, (b[4]||0) - zone, b[5]||0, ((b[6]||'0')+'00').slice(0,3)));
  } else {
    return new Date(         b[0], (b[1]? b[1]-1 : 0), b[2]||1, b[3]||0, b[4]||0,          b[5]||0, ((b[6]||'0')+'00').slice(0,3));
  }
}

// If precise is true, then time is used, otherwise only the
// date difference is returned
function dateDiff(startDate, endDate, precise) {

  // If start is after end, swap dates
  if (startDate > endDate) {
    var t = startDate;
    startDate = endDate;
    endDate = t;
  }
  
  var years = months = days = hrs = mins = secs = ms = 0;
  var d0 = new Date(+startDate), d1 = new Date(+endDate);
  var dx;
 
  // Add years to a date. Only tricky one is Feb 29. 
  function addYears(date, years) {
    var startMonth = date.getMonth();
    date.setFullYear(date.getFullYear() + years);
    // Catch Feb 29 -> March 1 
    if (date.getMonth() != startMonth) {
      date.setDate(0);
    }
    return date;
  }

  // Add months to a date. If goes beyond end of month when it shouldn't, trim
  // to last day of epxected month (e.g. 31 May + 1 => 31 June => 1 July => 30 June)
  function addMonths(date, months){
    var t = new Date(+date);
    var startDate = date.getDate();
    date.setMonth(date.getMonth() + +months);
    // If gone past end of month, set to last day of previous month
    if (date.getDate() != startDate) {
      date.setDate(0);
    }
    return date;
  }
  
  // Return number of days in month for date
  function daysInMonth(date) {
    var d = new Date(+date);
    d.setMonth(d.getMonth() + 1, 0);
    return d.getDate();
  }
  
  // If precision not required, set hours to start of day
  if (!precise) {
    d0.setHours(0,0,0,0);
    d1.setHours(0,0,0,0);
  }
  
  // Temp date for doing initial estimate
  dx = new Date(+d0);
  
  // Get year difference, add to early date (use temp date)
  years = d1.getFullYear() - d0.getFullYear();
  addYears(dx, years);

  // If gone past end, subtract 1 and update real early date
  if (dx > d1) years -= 1;
  addYears(d0, years);
  
  // Reset temp date
  dx = new Date(+d0);
  
  // Do months as for years
  months = ((d1.getMonth() + 12) - d0.getMonth()) % 12;
  // If month difference is zero but days are more than 360, must be next year so make months 11
  months = (d1 - d0) / 8.64e7 > 330? 11 : months;
  
  addMonths(dx, months);
  
  if (dx > d1) months -= 1;
  addMonths(d0, months);
  dx = new Date(+d0);
  
  // From here everything is predictable (even daylight saving, as long as the UA gets it right)
  var diff = d1 - d0;
  days  =  diff / 8.64e7 | 0;
  hrs   = (diff % 8.64e7) / 3.6e6 | 0;
  mins  = (diff % 3.6e6)  / 6e4 | 0;
  secs  = (diff % 6e4)    / 1e3 | 0;
  ms    =  diff % 1e3;

  return {y:years, m:months, d:days, hr:hrs, min:mins, sec:secs, ms:ms};  
}


// Run date tests
function sameAs(a0, a1) {
  return a0.every(function (v, i){return a1[i] == v});
}
var resultHTML = ['<table><tr><th>#<th>Start<th>End<th>Precise<th>Expect<th>Got'];
[
//*
  {start:'2016-01-31T00:00:00', end:'2016-01-31T00:00:00', precise: true, expect:  [0,0,0,0,0,0,0]},
  {start:'2016-01-31T00:00:00', end:'2016-01-31T00:00:00', precise: false, expect: [0,0,0,0,0,0,0]},

  {start:'2016-01-31T00:00:00', end:'2017-03-01T00:00:00', precise: true, expect:  [1,1,1,0,0,0,0]},
  {start:'2016-01-31T00:00:00', end:'2017-03-01T00:00:00', precise: false, expect: [1,1,1,0,0,0,0]},

  {start:'2016-01-29T00:00:00', end:'2017-03-01T00:00:00', precise: true, expect:  [1,1,1,0,0,0,0]},
  {start:'2016-01-29T00:00:00', end:'2017-03-01T00:00:00', precise: false, expect: [1,1,1,0,0,0,0]},

  {start:'2016-02-29T00:00:00', end:'2017-02-28T00:00:00', precise: true, expect:  [1,0,0,0,0,0,0]},
  {start:'2016-02-29T00:00:00', end:'2017-02-28T00:00:00', precise: false, expect: [1,0,0,0,0,0,0]},
//*/
  {start:'2016-02-29T00:00:01.1Z', end:'2017-02-28T00:00:00Z', precise: true, expect:  [0,11,29,23,59,58,900]},
  {start:'2016-02-29T00:00:01.1Z', end:'2017-02-28T00:00:00Z', precise: false, expect: [1,0,0,0,0,0,0]}, //*,

  {start:'2016-02-28T00:00:01.1', end:'2017-02-28T00:00:00', precise: true, expect:  [0,11,30,23,59,58,900]},
  {start:'2016-02-28T00:00:01.1', end:'2017-02-28T00:00:00', precise: false, expect: [1,0,0,0,0,0,0]},

  {start:'2016-02-27T00:00:01.1', end:'2017-02-28T00:00:00', precise: true, expect:  [1,0,0,23,59,58,900]},
  {start:'2016-02-27T00:00:01.1', end:'2017-02-28T00:00:00', precise: false, expect: [1,0,1,0,0,0,0]},

  {start:'2016-01-30T00:00:00', end:'2017-03-01T00:00:00', precise: true, expect:  [1,1,1,0,0,0,0]},
  {start:'2016-01-30T00:00:00', end:'2017-03-01T00:00:00', precise: false, expect: [1,1,1,0,0,0,0]},
  
  {start:'2016-01-30T00:00:01.001', end:'2017-03-01T00:00:00', precise: true, expect:  [1,1,0,23,59,58,999]},
  {start:'2016-01-30T00:00:01.001', end:'2017-03-01T00:00:00', precise: false, expect: [1,1,1,0,0,0,0]},
  
  {start:'2016-01-31T00:00:01.001', end:'2017-03-01T00:00:00', precise: true, expect:  [1,1,0,23,59,58,999]},
  {start:'2016-01-31T00:00:01.001', end:'2017-03-01T00:00:00', precise: false, expect: [1,1,1,0,0,0,0]},
  
  {start:'2016-01-30T00:00:01.001', end:'2017-03-02T00:00:00', precise: true, expect:  [1,1,1,23,59,58,999]},
  {start:'2016-01-30T00:00:01.001', end:'2017-03-02T00:00:00', precise: false, expect: [1,1,2,0,0,0,0]},
  
  {start:'2016-10-30T00:00:00', end:'2016-12-31T00:00:00', precise: true, expect:  [0,2,1,0,0,0,0]},
  {start:'2016-10-30T00:00:00', end:'2016-12-31T00:00:00', precise: false, expect: [0,2,1,0,0,0,0]},

  {start:'2016-10-30T00:00:00.1', end:'2016-12-31T00:00:00', precise: true, expect:  [0,2,0,23,59,59,900]},
  {start:'2016-10-30T00:00:00.1', end:'2016-12-31T00:00:00', precise: false, expect: [0,2,1,0,0,0,0]},

  {start:'2016-10-31T00:00:00', end:'2016-12-30T00:00:00', precise: true, expect:  [0,1,30,0,0,0,0]},
  {start:'2016-10-31T00:00:00', end:'2016-12-30T00:00:00', precise: false, expect: [0,1,30,0,0,0,0]},
 
  {start:'2016-01-31T00:00:00Z', end:'2016-01-31T00:00:00Z', precise: true, expect: [0,0,0,0,0,0,0]},   // 0
  {start:'2016-01-31T00:00:00Z', end:'2016-01-31T00:00:00Z', precise: false, expect: [0,0,0,0,0,0,0]},  // 1

  {start:'2016-01-31T00:00:00Z', end:'2017-03-01T00:00:00Z', precise: true,  expect: [1,1,1,0,0,0,0]},  // 2
  {start:'2016-01-31T00:00:00Z', end:'2017-03-01T00:00:00Z', precise: false, expect: [1,1,1,0,0,0,0]},  // 3
 
  {start:'2016-01-29T00:00:00Z', end:'2017-03-01T00:00:00Z', precise: true,  expect: [1,1,1,0,0,0,0]},  // 4
  {start:'2016-01-29T00:00:00Z', end:'2017-03-01T00:00:00Z', precise: false, expect: [1,1,1,0,0,0,0]},  // 5

  {start:'2016-01-30T00:00:00Z', end:'2017-03-01T00:00:00Z', precise: true,  expect: [1,1,1,0,0,0,0]},  // 6
  {start:'2016-01-30T00:00:00Z', end:'2017-03-01T00:00:00Z', precise: false, expect: [1,1,1,0,0,0,0]},  // 7
 
  {start:'2016-01-30T00:00:01Z', end:'2017-03-01T00:00:00Z', precise: true,  expect: [1,1,0,23,59,59,0]},  // 8
  {start:'2016-01-30T00:00:01Z', end:'2017-03-01T00:00:00Z', precise: false, expect: [1,1,1,0,0,0,0]},  // 9
 
  {start:'2016-10-30T00:00:00Z', end:'2016-12-31T00:00:00Z', precise: true,  expect: [0,2,1,0,0,0,0]},  // 10
  {start:'2016-10-30T00:00:00Z', end:'2016-12-31T00:00:00Z', precise: false, expect: [0,2,1,0,0,0,0]},  // 11

  {start:'2016-10-31T00:00:00Z', end:'2016-12-30T00:00:00Z', precise: true,  expect: [0,1,30,0,0,0,0]},  // 12
  {start:'2016-10-31T00:00:00Z', end:'2016-12-30T00:00:00Z', precise: false, expect: [0,1,30,0,0,0,0]},  // 13
 
  {start:'2016-10-31T00:00:01Z', end:'2016-12-30T00:00:00Z', precise: true,  expect: [0,1,29,23,59,59,0]},  // 14
  {start:'2016-10-31T00:00:01Z', end:'2016-12-30T00:00:00Z', precise: false,  expect: [0,1,30,0,0,0,0]},  // 15
 
  {start:'2016-10-31T00:00:00Z', end:'2016-12-31T00:00:00Z', precise: true,  expect: [0,2,0,0,0,0,0]},  // 16
  {start:'2016-10-31T00:00:00Z', end:'2016-12-31T00:00:00Z', precise: false, expect: [0,2,0,0,0,0,0]},  // 17
 
  {start:'2016-10-31T00:00:00Z', end:'2016-12-01T00:00:00Z', precise: true, expect: [0,1,1,0,0,0,0]},  // 18
  {start:'2016-10-31T00:00:00Z', end:'2016-12-01T00:00:00Z', precise: false,  expect: [0,1,1,0,0,0,0]},  // 19
 
  {start:'2016-10-31T00:00:01Z', end:'2016-12-01T00:00:00Z', precise: true,  expect: [0,1,0,23,59,59,0]},  // 20
  {start:'2016-01-31T12:00:01Z', end:'2016-03-02T12:00:00Z', precise: false, expect: [0,1,2,0,0,0,0]},  // 21

  {start:'2016-01-31T12:00:01Z', end:'2016-03-02T12:00:00Z', precise: true,  expect: [0,1,1,23,59,59,0]},  // 22
  {start:'2016-01-31T12:00:01Z', end:'2016-03-01T12:00:00Z', precise: false, expect: [0,1,1,0,0,0,0]},  // 23

  {start:'2016-01-31T12:00:01Z', end:'2016-03-01T12:00:00Z', precise: true,  expect: [0,1,0,23,59,59,0]},  // 24
  {start:'2016-01-31T12:00:01Z', end:'2016-03-01T12:00:00Z', precise: false,  expect: [0,1,1,0,0,0,0]},  // 25

  {start:'2016-01-30T12:00:01Z', end:'2016-03-01T12:00:00Z', precise: true,  expect: [0,1,0,23,59,59,0]},   // 26
  {start:'2016-01-30T12:00:01Z', end:'2016-03-01T12:00:00Z', precise: false, expect: [0,1,1,0,0,0,0]},  // 27

  {start:'2016-01-31T12:00:01Z', end:'2016-03-02T12:00:00Z', precise: true,  expect: [0,1,1,23,59,59,0]},  // 28
  {start:'2016-01-31T12:00:01Z', end:'2016-03-02T12:00:00Z', precise: false, expect: [0,1,2,0,0,0,0]},  // 29

  {start:'2016-01-30T12:00:01Z', end:'2016-03-02T12:00:00Z', precise: true,  expect: [0,1,1,23,59,59,0]},  // 30
  {start:'2016-01-30T12:00:01Z', end:'2016-03-02T12:00:00Z', precise: false, expect: [0,1,2,0,0,0,0]},     // 31
 
  {start:'2016-03-15T12:00:00Z', end:'2016-04-14T12:00:01Z', precise: true,  expect: [0,0,30,0,0,1,0]},   // 32
  {start:'2016-03-15T12:00:00Z', end:'2016-04-14T12:00:01Z', precise: false,  expect: [0,0,30,0,0,0,0]},  // 33

  {start:'2016-07-23T12:00:01Z', end:'2016-09-24T12:00:00Z', precise: true,  expect: [0,2,0,23,59,59,0]},  // 34
  {start:'2016-07-23T12:00:01Z', end:'2016-09-24T12:00:00Z', precise: false,  expect: [0,2,1,0,0,0,0]},    // 35

  {start:'2016-07-31T12:00:01Z', end:'2018-07-01T12:00:00Z', precise: true,  expect: [1,11,0,23,59,59,0]},
  {start:'2016-07-31T12:00:01Z', end:'2018-07-01T12:00:00Z', precise: false,  expect: [1,11,1,0,0,0,0]},

  {start:'2016-07-01T12:00:01Z', end:'2018-07-31T12:00:00Z', precise: true,  expect: [2,0,29,23,59,59,0]},
  {start:'2016-07-01T12:00:01Z', end:'2018-07-31T12:00:00Z', precise: false,  expect: [2,0,30,0,0,0,0]}
//*/
  ].forEach(function(obj, i) {
  var x = dateDiff(parseISO(obj.start), parseISO(obj.end), obj.precise)
  var r = [x.y, x.m, x.d, x.hr, x.min, x.sec, x.ms];
  resultHTML.push('<tr><td>'+i+'<td>' + obj.start + '<td>' + obj.end + '<td>' + (obj.precise? 'Y':'') +
                  '<td>' + (obj.precise? obj.expect : obj.expect.slice(0,3)) + '<td class="' + (sameAs(r, obj.expect)? '':'fail') + '">' +
                  (obj.precise? r : r.slice(0,3)));
});
document.write(resultHTML.join('') + '</table>');
.fail {
  background-color: #ffbbbb;
}
table {
  font-family: courier, mono-spaced;
  font-size: 90%;
  border-collapse: collapse;
  border-top: 1px solid #bbbbbb;
  border-left: 1px solid #bbbbbb;
}
td, th {
  border-bottom: 1px solid #bbbbbb;
  border-right: 1px solid #bbbbbb;
}
td {
  padding: 2px  5px 2px 5px;
}
td:nth-Child(3) {
  text-align: center;
}
RobG
  • 142,382
  • 31
  • 172
  • 209