skip to Main Content

I am asking end user to put numerical age of the user. For example user inputs 31.

Behind the scene I convert this to the year and store it. The logic is below to convert:

 (moment().subtract(age, 'years')).year();

Which returns the value 1993

Good so far. However, in the other part of the UI where i show this age as number it shows 30 rather 31. The code that converts it back is like below:

 let birthYear =  this.userData.profile.birthYear
     let currYear = moment().year()
     let currDate = moment([currYear, 1, 1])
     let birthDate = moment([birthYear, 1, 1])
     var diffDuration = moment.duration(currDate.diff(birthDate));
     age = diffDuration.years()

I am lost why the age year becomes 30 rather 31.

2

Answers


  1. Your diffDuration is 30 years, 11 month and 29 days; .years() gets 30 indeed, but to get the full duration, one has to also check .months() -> 11, .days() -> 29, .hours() -> 0 , .minutes() -> 0, .seconds() -> 0, .milliseconds() -> 0.

    The following snippet shows that, using the properties of the object diffDuration._data, which are those returned by the functions mentioned above:

    const age = 31,
        birthYear = (moment().subtract(age, 'years')).year(),
        currYear = moment().year(),
        currDate = moment([currYear, 1, 1]),
        birthDate = moment([birthYear, 1, 1]),
        dateDiff = currDate.diff(birthDate),  // note that this is a number, i.e., the milliseconds of the diff
        diffDuration = moment.duration(dateDiff),
        durationText = Object.entries(diffDuration._data)
            .reverse() // start from the largest unit (years) to smaller ones
            .filter(([_, value]) => value > 0) // don't show hours, minutes, etc., that have zero value
            .map(([unit, value ]) => `${value} ${unit}`)
            .join(' ');
    console.log(durationText)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>

    If you add one day, i.e., currDate = moment([currYear, 1, 2]), you’ll get the full 31 years:

    const age = 31,
        birthYear = (moment().subtract(age, 'years')).year(),
        currYear = moment().year(),
        currDate = moment([currYear, 1, 2]),
        birthDate = moment([birthYear, 1, 1]),
        dateDiff = currDate.diff(birthDate),  // note that this is a number, i.e., the milliseconds of the diff
        diffDuration = moment.duration(dateDiff),
        durationText = Object.entries(diffDuration._data)
            .reverse() // start from the largest unit (years) to smaller ones
            .filter(([_, value]) => value > 0) // don't show hours, minutes, etc., that have zero value
            .map(([unit, value ]) => `${value} ${unit}`)
            .join(' ');
    console.log(durationText)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>

    Now, why is a day missing? Note that when you compute currDate.diff(birthDate), that’s just a number of milliseconds; you loose the actual start and end moments by constructing a Duration object. It boils down to the count of leap years.

    There are 7 leap years in your interval of 31 years; but it could have been 8 leap years, e.g., 1994->2025. Since you don’t have the start and end dates, the system doesn’t know which one it is. Actually, the average number of leap years in any interval of 31 years is slightly less than 7.75 (31/4), so the library rounds it
    up to the closest value, 8 (since it has to be either 7 or 8) and since it expects 8 leap years, it’s 31 years minus one day.

    This is implemented in the source code in the function daysToMonths. For the interval you have, there are 11322 days converted to 371.9829975974866 months, slightly less than the required 372 months for 31 years.

    The solution, if you want to just use .years() as you already have,
    is to just move the starting moment of the interval at least one day later, or several days later, you may even go to:

    const currDate = moment([currYear, 6, 1]);
    const birthDate = moment([birthYear, 1, 1]);
    moment.duration(currDate.diff(birthDate)).years() //--> 31
    

    you can’t get more than the correct number of years.

    Login or Signup to reply.
  2. The problem

    From the moment.js docs:

    Durations do not have a defined beginning and end date. They are contextless.
    A duration is conceptually more similar to ‘2 hours’ than to ‘between 2 and 4 pm today’. As such, they are not a good solution to converting between units that depend on context.
    For example, a year can be defined as 366 days, 365 days, 365.25 days, 12 months, or 52 weeks.

    Your issue stems from passing the result of diff() to duration(), which doesn’t behave reliably for the reason given in the docs. The duration object itself is awfully close to a year:

      _data: {
        milliseconds: 0,
        seconds: 0,
        minutes: 0,
        hours: 0,
        days: 29,
        months: 11,
        years: 30
      },
    

    But when you extract the 'years' key it just gives you the 30 value in the duration object without rounding up.

    The solution

    Just pass 'years' as a second argument to diff(), which sets the unit of measurement, and don’t bother with duration() at all, like this:

    let currYear = moment().year()
    let currDate = moment([currYear, 1, 1])
    let birthDate = moment([birthYear, 1, 1])
    
    age = currDate.diff(birthDate, 'years');
    

    Which sets age to the desired 31.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search