1

I have 2 array of date ranges I'm trying to find the difference between. Let's use numbers for example:

I have 2 ranges [1-7, 9-16] and I want to subtract [2-3, 7-9, 14-20] and get the result ranges of [1-1, 4-6, 10-13]

I'm getting caught in a bit of a rut trying to figure it out. Surely there's a common solution to this that I'm unaware of?

diffDateRangesArray(rangesArray1, rangesArray2) {
    //rangesArray = [{startDate, endDate}]
    let diffedRanges = [];
    rangesArray1.forEach(function(range1){
      //loop through rangesArray2 removing from range1
      rangesArray2.forEach(function(range2){
        // breaks if array returned
        // perhaps should always return array and flatten?
        range1 = diffDateRanges(range1, range2);
      });
      diffedRanges.push(range1);
    });
    //probably should do some sort of union here
    return diffedRanges;
  }

  diffDateRanges(range1, range2) {
    //range = {startDate, endDate}
    let diffedRange = {};
    // if not in range
    if(range2.endDate <= range1.startDate || range2.startDate >= range1.endDate){
      return range1;
    //if envelops range
    } else if(range2.endDate >= range1.endDate && range2.startDate <= range1.startDate){
      return null;
    //if cuts off end of range
    } else if(range2.startDate <= range1.endDate && range2.endDate >= range1.endDate){
      return {startDate:range1.startDate, endDate: range2.startDate};
    // if cuts off start of range
    } else if(range2.endDate >= range1.startDate && range2.startDate <= range1.startDate){
      return {startDate:range2.endDate, endDate: range1.endDate};
    // if inside of range - should better handle arrays
    } else if(range2.startDate >= range1.startDate && range2.endDate <= range1.endDate){
      return [
        {startDate:range1.startDate, endDate: range2.startDate},
        {startDate:range2.endDate, endDate: range1.endDate},
      ];
    }
  }
Ryan King
  • 3,538
  • 12
  • 48
  • 72
  • I didn't get how subtracting `[2-3, 7-9, 14-20]` from `[1-7, 9-16]` would result in `[1-1, 4-6, 10-13]`... could you explain this part in more detail, what is getting subtracted from what and such? – Marcus Vinícius Monteiro Feb 26 '17 at 13:28
  • How about: I have the numbers `[1,2,3,4,5,6,7,9,10,11,12,13,14,15,16]` and I would like to remove `[2,3,7,8,9,14,15,16,17,18,19,20]`, resulting in `[1,4,5,6,10,11,12,13]` - however the input and output needs to be an object with `{start, end}` rather than a series. This may have given me an idea... – Ryan King Feb 26 '17 at 13:36
  • I suppose I just need to translate the `{start, end}` object into an array and difference them! Then translate it back to a `{start, end}` array – Ryan King Feb 26 '17 at 13:38
  • I would consider writing a helper function like.. `function overlaps(happy, unhappy) { if (happy.start <= unhappy.start && unhappy.end <= happy.end) return 3; // unhappy in happy if (happy.start <= unhappy.start && unhappy.start < happy.end) return 1; // unhappy starts in happy if (happy.start < unhappy.end && unhappy.end <= happy.end) return 2; // unhappy ends in happy if (unhappy.start <= happy.start && happy.end <= unhappy.end) return 4; // happy in unhappy if (happy.start <= unhappy.end) return 0; // happy before unhappy return 8; // happy after unhappy }` – Paul S. Feb 26 '17 at 13:59

2 Answers2

1

If I understood you question correctly, you can accomplish what you want with the following:

Let us first make some utility functions:

function range(start, end) {
  return [...Array(end - start + 1)].map((_, i) => start + i)
}

function unique(a) {
  return Array.from(new Set(a))
}

function immutableSort(arr) {
  return arr.concat().sort((a, b) => a - b)
}

Array.prototype.has = function(e) {
  return this.indexOf(e) >= 0
}

Object.prototype.isEmpty = function() {
  return Object.keys(this).length === 0 && this.constructor === Object
}

function arrayDifference(A, B) {
  return A.filter((e) => B.indexOf(e) < 0)
}

Now, let's make some functions to tackle your specific problem:

function arrayToRangeObjects(A) {
  const preparedA = immutableSort(unique(A))
  const minA = preparedA[0]
  const maxA = preparedA[preparedA.length - 1]

  const result = []
  let rangeObject = {}
  range(minA, maxA).forEach((v) => {
    if (!preparedA.has(v)) {
      if (rangeObject.hasOwnProperty('start')) {
          if (!rangeObject.hasOwnProperty('end')) {
            rangeObject.end = rangeObject.start
          }
        result.push(rangeObject)
      }
      rangeObject = {}
    } else {
      if (rangeObject.hasOwnProperty('start')) {
        rangeObject.end = v
      } else {
        rangeObject.start = v
      }
    }
  })
  if (!rangeObject.isEmpty()) {
    result.push(rangeObject)
  }
  return result
}

function rangeObjectToRange(rangeObject) {
  return range(rangeObject.start, rangeObject.end)
}

function rangeObjectsToRange(A) {
  return immutableSort(
    unique(
      A
      .map((rangeObject) => {
        return rangeObjectToRange(rangeObject)
      })
      .reduce((a, b) => {
        return a.concat(b)
      }, [])
    )
  )
}

With that, the answer to your problem is:

function yourAnswer(A, B) {
  return arrayToRangeObjects(
    arrayDifference(rangeObjectsToRange(A), rangeObjectsToRange(B))
  )
}

Let's test it:

const A = [
  {
    start: 1,
    end: 7
  },
  {
    start: 9,
    end: 16
  }
]

const B = [
  {
    start: 2,
    end: 3
  },
  {
    start: 7,
    end: 9
  },
  {
    start: 14,
    end: 20
  }
]

> yourAnswer(A, B)
[
  {
    start: 1,
    end: 1
  },
  {
    start: 4,
    end: 6
  },
  {
    start: 10,
    end: 13
  }
]

Just as personal opinion, I believe this "range object" data structure of yours is a bit difficult to work it, and a little inflexible (all this hassle just to get the ranges that don't overlap with a collection of ranges): you might want to take look at more efficient data structures for storing ranges.

Community
  • 1
  • 1
  • You wouldn't have an example on how to do this with an r-tree would you? – Ryan King Feb 26 '17 at 20:36
  • 1
    @RyanKing I think this [`xspans`](https://github.com/exjs/xspans) library you referenced in your answer can do what you want, if you're thinking about intervals. Very nice API: `sub`, `and`, `toObjects`... I liked it. – Marcus Vinícius Monteiro Feb 28 '17 at 17:36
1

So turns out this is called Interval Algebra and there's libraries for this https://www.npmjs.com/package/qintervals

Ryan King
  • 3,538
  • 12
  • 48
  • 72