How to identify if a date is between October 1 to May 31 irrespective of the year in Javascript

Viewed 147

So I am working on a JavaScript application where I need to calculate monthly charges of utilities consumed. The utility bills have different rates

  • winter rates (June 1 to September 30) --> higher rates
  • summer rates (October 1 - May 31)

So when I need to calculate a bill for the month of lets say : April 25 - May 22 then this needs to be determined as Summer.
So I need to calculate if the billing date is in summer or in winter.

I am trying to do some something of the following form , but this is a lot of juggling in code.

How do I calculate if a given date in between say October - May ( Summer ) or between June - September ( winter )

Note - I need to do this ignoring year

if (
  dateFrom.getMonth() >= vendorSummerStartDate.getMonth() &&
  dateFrom.getDate() >= vendorSummerStartDate.getDate() &&
  dateTo.getMonth() <= vendorSummerEndDate.getMonth() &&
  dateTo.getDate() <= vendorSummerEndDate.getDate()
) { /*... */ }
2 Answers

It looks tricky when the start month could be a greater number than the end month. We can take abstraction of this, and just consider the involved values (start, end, value) to be any integers (not necessarily months):

  • Check whether the given value lies on or between the start and end values, by checking that value - start and value - end have a different sign, or more precisely, that the product of these terms is either 0 or negative. If 0, then we can conclude the value coincides with either start or end value and is thus in the range.

  • Otherwise check whether the start value is less than the end value

  • If both the above boolean expressions are equal (true - true or false - false), then we can conclude that the value is in the range.

Implementation:

function isInRange (start, end, val) {
    let sign = (val - start) * (val - end);
    return !sign || (sign < 0) == (start < end); 
}

// Wrapper around above function, converting dates to months
const monthIsInRange = (...args) => isInRange(...args.map(dt => dt.getMonth()));

// A few tests:
console.log(monthIsInRange(new Date("2019-06-01"), new Date("2019-09-30"), 
                           new Date("2021-07-15"))); // true
console.log(monthIsInRange(new Date("2019-06-01"), new Date("2019-09-30"), 
                           new Date("2021-05-15"))); // false
console.log(monthIsInRange(new Date("2019-10-01"), new Date("2020-05-31"), 
                           new Date("2021-07-15"))); // false
console.log(monthIsInRange(new Date("2019-10-01"), new Date("2020-05-31"), 
                           new Date("2021-05-15"))); // true

Updated This post has a runnable snippet page that has a "is this date within a date-range that possibly-wraps-around?" calculator in it:

function compare_month_and_day_only( x, y ) {
    let cmp = x.getUTCMonth() - y.getUTCMonth();
    if( cmp != 0 ) return cmp;
    cmp = x.getUTCDay() - y.getUTCDay();
    return cmp;
}

function dt_is_in_simple_range( start, end, dt ) {
    // Preconditions:
    if( end <= start ) throw new Error( "End must be after Start" );
    if( compare_month_and_day_only( start, end ) > 0 ) throw new Error( "End must be positioned after Start in a flat calendar." );
    
    const s2 = new Date( dt.getUTCFullYear(), start.getUTCMonth(), start.getUTCDay() );
    const e2 = new Date( dt.getUTCFullYear(), end.getUTCMonth(), end.getUTCDay() );
    
    return ( s2 <= dt ) && ( dt <= e2 );
}

function dt_is_in_range( start, end, dt ) {
    // Preconditions:
    if( end <= start ) throw new Error( "End must be after Start" );
    
    const range_wraps_around = compare_month_and_day_only( start, end ) > 0;
    if( range_wraps_around ) {
        // Invert the date-range - and invert the result:
        const invStart = new Date( dt.getUTCFullYear(), end.getUTCMonth(), end.getUTCDay() );
        const invEnd   = new Date( dt.getUTCFullYear(), start.getUTCMonth(), start.getUTCDay() );
        const is_in_inverted_range = dt_is_in_simple_range( invStart, invEnd, dt );
        return !is_in_inverted_range;
    }
    else {
        return dt_is_in_simple_range( start, end, dt );
    }
}

//

function updateResults() {
   
    // Most of this code is just to create dates-from-numbers as JavaScript makes this surprisingly verbose and difficult.
    const subject = document.getElementById('subjectDate').valueAsDate;
    if( !subject ) return;
    
    const startMon = document.getElementById('startDateM').valueAsNumber;
    const startDay = document.getElementById('startDateD').valueAsNumber;
    
    const endMon   = document.getElementById('endDateM'  ).valueAsNumber;
    const endDay   = document.getElementById('endDateD'  ).valueAsNumber;
    
    const year = subject.getUTCFullYear();
    
    const start    = new Date( year, startMon, startDay );
    let   end      = new Date( year, endMon  , endDay   );
    if( end < start ) {
        end = new Date( year + 1, endMon, endDay );
    }
    
    if( !start || !end || !subject ) return;
    
    let resultMessage = '';
    try {
      const result = dt_is_in_range( start, end, subject );
      resultMessage = result ? "Yes" : "No";
    }
    catch( err ) {
      resultMessage = err.message || 'Other error';
    }
    
    const li = document.createElement('li');
    
    li.textContent = ( "Is " + subject.toLocaleDateString() + " in the range " + start.toLocaleDateString() + " to " + end.toLocaleDateString() + "? " + resultMessage );
    
    document.getElementById('list').appendChild( li );

}
<label>
  Start day
  <input type="number" id="startDateD" min="1" max="31" oninput="updateResults()" />
</label>
<label>
  Start month
  <input type="number" id="startDateM" min="1" max="12" oninput="updateResults()" />
</label>


<label>
  End day
  <input type="number" id="endDateD" min="1" max="31" oninput="updateResults()" />
</label>
<label>
  End month
  <input type="number" id="endDateM" min="1" max="12" oninput="updateResults()" />
</label>


<label>
  Subject date
  <input type="date" id="subjectDate" oninput="updateResults()" />
</label>

<ul id="list">
</ul>


It's easy to over-think a puzzle like this, but a simpler approach is to simply check if it's a wrap-around date-range or not; if it is, then start is after end in the year - a date in that range is the same as not being the range from end to start` in the same year. So just normalize the inputs to a simple single-year range and invert if necessary.

enter image description here

(My apologies for using snake_case here, I feel it works better for situations like these)

function compare_month_and_day_only( x, y ) {
    let cmp = x.getUTCMonth() - y.getUTCMonth();
    if( cmp != 0 ) return cmp;
    cmp = x.getUTCDay() - y.getUTCDay();
    return cmp;
}

function dt_is_in_simple_range( start, end, dt ) {
    // Preconditions:
    if( end <= start ) throw new Error( "End must be after Start" );
    if( compare_month_and_day_only( start, end ) > 0 ) throw new Error( "End must be positioned after Start in a flat calendar." );
    
    const s2 = new Date( dt.getUTCYear(), start.getUTCMonth(), start.getUTCDay() );
    const e2 = new Date( dt.getUTCYear(), end.getUTCMonth(), end.getUTCDay() );
    
    return ( s2 <= dt ) && ( dt <= e2 );
}

function dt_is_in_range( start, end, dt ) {
    // Preconditions:
    if( end <= start ) throw new Error( "End must be after Start" );
    
    const range_wraps_around = compare_month_and_day_only( start, end ) > 0;
    if( range_wraps_around ) {
        // Invert the date-range - and invert the result:
        const invStart = new Date( dt.getUTCYear(), end.getUTCMonth(), end.getUTCDay() );
        const invEnd   = new Date( dt.getUTCYear(), start.getUTCMonth(), start.getUTCDay() );
        const is_in_inverted_range = dt_is_in_simple_range( invStart, invEnd, dt );
        return !is_in_inverted_range;
    }
    else {
        return dt_is_in_simple_range( invStart, invEnd, dt );
    }
}

Tip: Documenting preconditions (with runtime assertions) makes it easier to understand the "shape" of data a function receives - not just like types in TypeScript, but also the domain and range of values - which isn't something you can easily express in TypeScript.

Related