2

I have the following problem. I have an array:

const dates = ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019", "14-June-2019"]

I need to generate an array of date that are after I mean dates in row. So:

const datesInRow = ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019"]

Here is my initial function: (isAfter function = date-fns)

 function numberOfDaysInRow(arr) {
  if (arr.length === 1) return arr;
  let numberOfDaysInRowArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (isAfter(new Date(arr[i]), new Date(arr[i + 1]))) {
      numberOfDaysInRowArr.push(arr[i]);
    }
  }
  return numberOfDaysInRowArr;
}

This returns only partial answer. For example If I have only two string in array like this:

const dates = ["19-June-2019", "18-June-2019"]

It will return

["19-June-2019"]

which is not what I want.

7
  • Do you mean that you want the array to only include dates that are 1 day apart from the next? Commented Jun 19, 2019 at 19:27
  • what if you have holes in the data? Commented Jun 19, 2019 at 19:28
  • yes only dates that are 1 day apart from next. exactly. Commented Jun 19, 2019 at 19:29
  • please add some more use cases. Commented Jun 19, 2019 at 19:38
  • Yea, as @NinaScholz says, what you expect to get from this input: ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019", "14-June-2019", "10-June-2019", "09-June-2019"] for example... Will strings 10-June-2019 and 09-June-2019 be included on the final array? Commented Jun 19, 2019 at 19:45

6 Answers 6

2

const dates = ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019", "14-June-2019"]
const datesInRow = ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019"]
const datesBroken = ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019", "10-June-2019", "09-June-2019", "08-June-2019"]

function isAfter(date1, date2) {
  return (date1.getTime() - date2.getTime()) == 86400000;
}

function numberOfDaysInRow(arr) {
  if (arr.length === 1) return arr;
  let numberOfDaysInRowArr = [arr[0]];
  for (let i = 0; i < arr.length - 1; i++) {
    if (isAfter(new Date(arr[i]), new Date(arr[i + 1]))) {
      numberOfDaysInRowArr.push(arr[i + 1]);
    } else {
      i = arr.length - 1;
    }
  }
  return numberOfDaysInRowArr;
}

console.log(numberOfDaysInRow(dates))
console.log(numberOfDaysInRow(datesInRow))
console.log(numberOfDaysInRow(datesBroken))

You should initialize numberOfDaysInRowArr with [arr[0]].

And loop i until arr.length - 1.

Also, push arr[i + 1] instead of arr[i].

Sign up to request clarification or add additional context in comments.

3 Comments

@RobG, thanks for your comment. But isAfter is not the point of my answer. isAfter function is already defined and used for @aphex who posted this question. I made isAfter myself to show @aphex to get the idea out of isAfter function.
The OP's use of date-fns' isAfter is also an issue that should be highlighted as it doesn't do what the OP needs.
Thanks to both of you and thanks for clarification about isAfter function.
2

You could treat the first element outside of the loop and take only the next element.

function numberOfDaysInRow(arr) {
    if (arr.length === 1) return arr;
    let numberOfDaysInRowArr = [];

    if (isAfter(new Date(arr[0]), new Date(arr[1]))) {
        numberOfDaysInRowArr.push(arr[0]);
    }

    for (let i = 0; i < arr.length; i++) {
        if (isAfter(new Date(arr[i]), new Date(arr[i + 1]))) {
            numberOfDaysInRowArr.push(arr[i + 1]);
        }
    }
    return numberOfDaysInRowArr;
}

An abstract approach by using a state variable with the following values:

  • 0: initial value or no following date found
  • 1: following date found
  • 2: sequence end

This approach takes integer values instead of date the order is ascending instead of descending.

It takes only the first found contiguous data set.

var array = [1, 3, 5, 7, 8, 9, 10, 12, 14],
    state = 0,
    result = array.filter((v, i, { [i - 1]: prev, [i + 1]: next }) => {
        switch (state) {
            case 0:
                if (v + 1 === next) {
                    state = 1;
                    return true;
                }
                break;
            case 1:
                if (prev + 1 === v) return true;
                state = 3;
                break
        }
    });

console.log(result);

Comments

1

You can check if is your last element...

function numberOfDaysInRow(arr) {
  if (arr.length === 1) return arr;
  let numberOfDaysInRowArr = [];
  for (let i = 0; i < arr.length; i++) {
    const isTheLastElement = i === arr.length - 1;
    const isAfterNext = () => isAfter(new Date(arr[i]), new Date(arr[i + 1])); 
    if (isTheLastElement || isAfterNext()) {
      numberOfDaysInRowArr.push(arr[i]);
    }
  }
  return numberOfDaysInRowArr;
}

Now, if you extract the condition (a predicate), you can use filter to write a cleaner version...

function isLastElementOrAfterNext(string, index, stringList) {
   return index === stringList.length - 1 || isAfter(new Date(string), new Date(stringList[index + 1];
}
function numberOfDaysInRow(arr) {
   return arr.filter(isLastElementOrAfterNext);
}

You can improve this example using map at first, to convert the strings in Dates...

function stringToDate(string) {
   return new Date(string);
}
function isLastElementOrAfterNext(date, index, dateList) {
   return index === dateList.length - 1 || isAfter(date, dateList[index + 1];
}
function numberOfDaysInRow(arr) {
   return arr.map(stringToDate).filter(isLastElementOrAfterNext);
}

Comments

1

Each date can be compared with the previous and next date to check if they are a day apart :

const dates = ["6-June-2019", "4-June-2019", "3-June-2019", "1-June-2019"]

const filtered = dates.filter((v, i, a) => new Date(v) - new Date(a[i - 1]) > -1e8
                                        || new Date(v) - new Date(a[i + 1]) <  1e8 )

console.log( filtered )


To handle multiple groups and get the largest one of them :

const dates = ["23-June-2019", "22-June-2019", 
               "13-June-2019", "12-June-2019", "11-June-2019", 
                                "2-June-2019",  "1-June-2019"];

let start = 0, maxCount = 0;

for (let count = 1, i = dates.length - 1; i >= 0; i--, count++) {
  if (i < 1 || new Date(dates[i - 1]) - new Date(dates[i]) > 1e8) {
    if (count > maxCount) {
      start = i; 
      maxCount = count;
    }
    count = 0; 
  }
}

console.log( dates.slice(start, start + maxCount) );


To find the first group :

const dates = ["23-June-2019", 
               "13-June-2019", "12-June-2019", 
                "3-June-2019",  "2-June-2019",  "1-June-2019"];

const start = dates.findIndex((v, i, a) => new Date(v) - new Date(a[++i]) < 1e8);
const end = dates.findIndex((v, i, a) => i > start && new Date(v) - new Date(a[++i]) > 1e8);

console.log( dates.slice(start, end + 1) );

2 Comments

This is very interest but at first it is really hard to read what is happening inside this function. It solves the problem :)
@aphex I updated for cases when the first date is not a day apart. It checks if the difference with the previous or next date is a day apart, and 864e5 is the milliseconds in a day
1

You should not use the built–in parser for the timestamps as the format DD-MMM-YYYY is not supported by ECMA-262. Also, the date-fns isAfter function just tells you if the first date is after the second, it doesn't tell you by how much. A more appropriate function would be differenceInDays.

date-fns doesn't have its own parser, it uses the built-in parser so I'd suggest using a different library. Parsers have been covered many times in other questions but a parser for the OP format isn't difficult.

So just start with the first date and stop when you either get to the end of the array, or get a date that is more than one day before the previous date, e.g.

// Parser for format DD-MMM-YYYY
// Does not validate date
function parseDMY(s) {
  let months = 'jan feb mar apr may jun jul aug sep oct nov dec'.split(' ');
  let b = s.split('-');
  return new Date(b[2], months.indexOf(b[1].toLowerCase().substr(0,3)), b[0]);
}

// Return true if d1 is the day before d0
// where d0, d1 are Dates
function isPreviousDay(d0, d1) {
  // Copy dates and set to 00:00:00
  let t0 = new Date(d0);
  t0.setHours(0,0,0,0);
  let t1 = new Date(d1);
  t1.setHours(0,0,0,0);
  // Set t0 to previous day
  t0.setDate(t0.getDate() - 1);
  // See if they now have the same time value
  return t0.getTime() == t1.getTime();
}

function getContiguous(dates) {
  // Get timestamps as array of Dates
  let arr = dates.map(parseDMY);
  // Result starts with first timestamp
  let result = [dates[0]];
  // Stop comparing at second last date
  let len = dates.length - 2
  let i = 0;

  // While each date is the day before the current date,
  // add its timestamp to the result array
  while (i<len && isPreviousDay(arr[i], arr[i+1])) {
    result.push(dates[i+1]);
    i++;
  }
  return result;
}

let dates = ["19-June-2019", "18-June-2019", "17-June-2019", "16-June-2019", "14-June-2019"];

console.log(getContiguous(dates));
<script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js"></script>

I attached the date-fns library but it didn't seem to load correctly, so I wrote a bespoke isPreviousDay function.

1 Comment

Thank you for you explanation. I really appreciate.
0
if (isAfter(new Date(arr[i]), new Date(arr[i + 1]))) {
  numberOfDaysInRowArr.push(arr[i]);
}

If arr[i] is the last element in the array, arr[i+1] will be undefined, and nothing will be pushed to numberOfDaysInRowArr.

2 Comments

I know that. That is why I am asking here how to solve this issue.
Sorry, I wasn't sure exactly what you were asking. You could add a check for arr[i+1] being undefined, and if it is, instead compare arr[i] to arr[i-1]. There are many ways to do this, but this is a quick and dirty fix.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.