14

I see this question asked quite often for regular javascript arrays, however none of the answers seems to work if its an array of dates.

I can likely figure this out through trial an error but I do see some benefit to others if I ask.

Basically if you have a javascript array of dates that might have duplicates and need to filter into an array with no duplicates what is the best way to go about that?

I have tried the ES6 solution of Array.from(new Set(arr)) but it just returns the same array.

Also I tried

Array.prototype.unique = function() {
    var a = [];
    for (var i=0, l=this.length; i<l; i++)
        if (a.indexOf(this[i]) === -1)
            a.push(this[i]);
    return a;
}

both came from Unique values in an array

However neither worked, looks like indexOf does not work on date objects.

Here is how my array is generated atm

//this is an array generated from ajax data, 
//its a year over year comparison with a separate year, 
//so to create a reliable date objects I force it to use the same year.
data.map(d => {
   dp = new Date(Date.parse(d.date + '-' + d.year));
   dp.setFullYear(2000);
   return dp;
})

It is about 100 or so different days, but it always ends up with about 350 index's.

Community
  • 1
  • 1
Jordan Ramstad
  • 169
  • 3
  • 8
  • 37
  • This might help: http://stackoverflow.com/questions/492994/compare-two-dates-with-javascript – Maria Ines Parnisari Oct 31 '16 at 16:54
  • 1
    Put each of the dates in a Set first. – bhspencer Oct 31 '16 at 16:59
  • Definitely the right direction, just need to figure out a way to filter an array using that, would prefer to do it inline without creating other variables, I will play with the idea anyway, thank you @mparnisari – Jordan Ramstad Oct 31 '16 at 17:02
  • If I understand correctly you want a Set of data? https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set "Set objects are collections of values, you can iterate its elements in insertion order. A value in the Set may only occur once; it is unique in the Set's collection." – Algimantas Krasauskas Oct 31 '16 at 17:05
  • @JordanRamstad `.map` to serialise all of the Date objects into strings -> add them to a Set -> create new array from the Set -> `map` into Date objects. Alternatively simply `filter` by maintaining a lookup of all serialised Dates. possibly in a Set. So the filter function will be something like `if (Date_as_string in my_lookup) { discard } else { add it to the set and return true}` – VLAZ Oct 31 '16 at 17:07

6 Answers6

20

ES6 way:

datesArray.filter((date, i, self) => 
  self.findIndex(d => d.getTime() === date.getTime()) === i
)

Thanks to https://stackoverflow.com/a/36744732/3161291

Sergey Reutskiy
  • 2,781
  • 2
  • 18
  • 14
12

If you compare two dates via ===, you compare the references of the two date objects. Two objects that represent the same date still are different objects.

Instead, compare the timestamps from Date.prototype.getTime():

function isDateInArray(needle, haystack) {
  for (var i = 0; i < haystack.length; i++) {
    if (needle.getTime() === haystack[i].getTime()) {
      return true;
    }
  }
  return false;
}

var dates = [
  new Date('October 1, 2016 12:00:00 GMT+0000'),
  new Date('October 2, 2016 12:00:00 GMT+0000'),
  new Date('October 3, 2016 12:00:00 GMT+0000'),
  new Date('October 2, 2016 12:00:00 GMT+0000')
];

var uniqueDates = [];
for (var i = 0; i < dates.length; i++) {
  if (!isDateInArray(dates[i], uniqueDates)) {
    uniqueDates.push(dates[i]);
  }
}

console.log(uniqueDates);

Optimization and error handling is up to you.

TimoStaudinger
  • 41,396
  • 16
  • 88
  • 94
  • This works and the performance is fairly good at about 0.368ms for my dataset. – Jordan Ramstad Oct 31 '16 at 17:42
  • 1
    Please don't do this, unless you only have a small amount of dates, as in the example. The time complexity is quadratic. If you just use a `Set` of numbers storing the `getTime()` milliseconds, you can use the `has` operation which is amortized constant, making the full unique operation linear. – mlg Feb 24 '20 at 17:00
6

You can do a simple filter with a lookup but you need to convert the dates to something that can be compared, since two objects are never the same in JavaScript, unless it's two references to the exact same object.

const dates = [
  new Date(2016, 09, 30, 10, 35, 40, 0),
  new Date(2016, 09, 30, 10, 35, 40, 0), //same
  new Date(2016, 09, 30, 10, 35, 40, 0), //same
  new Date(1995, 07, 15, 03, 15, 05, 0) //different
];


function filterUniqueDates(data) {
  const lookup = new Set();
  
  return data.filter(date => {
     const serialised = date.getTime();
    if (lookup.has(serialised)) {
      return false;
    } else { 
      lookup.add(serialised);
      return true;
    }
  })
}

console.log(filterUniqueDates(dates));

This can be further generalised, if you want to filter anything by just changing how you determine uniqueness

const dates = [
  new Date(2016, 09, 30, 10, 35, 40, 0),
  new Date(2016, 09, 30, 10, 35, 40, 0), //same
  new Date(2016, 09, 30, 10, 35, 40, 0), //same
  new Date(1995, 07, 15, 03, 15, 05, 0) //different
];

const dateSerialisation = date => date.getTime(); // this is the previous logic for dates, but extracted

//as primitives, these can be compared for uniqueness without anything extra
const numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
const strings = ["a", "b", "b", "c", "c", "c"];

const people = [
  {name: "Alice", age: 20},
  {name: "Bob", age: 30},
  {name: "Bob", age: 40}, //technically the same
  {name: "Carol", age: 50},
  {name: "Carol", age: 60}, //technically the same
  {name: "Carol", age: 70} //technically the same
]

//let's assume that a person with the same name is the same person regardless of anything else 
const peopleSerialisation = person => person.name;

/* 
 * this now accepts a transformation function that will be used 
 * to find duplicates. The default is an identity function that simply returns the same item.
 */
function filterUnique(data, canonicalize = x => x) { 
  const lookup = new Set();
  
  return data.filter(item => {
     const serialised = canonicalize(item); //use extract the value by which items are considered unique
    
    if (lookup.has(serialised)) {
      return false;
    } else { 
      lookup.add(serialised);
      return true;
    }
  })
}


console.log("dates", filterUnique(dates, dateSerialisation));
console.log("numbers", filterUnique(numbers));
console.log("strings", filterUnique(strings));
console.log("people", filterUnique(people, peopleSerialisation));

This is using ES6 but it's trivial to convert to ES5 compliant code - removing the fat arrow functions, the default parameter and the new Set() here is what you need:

function filterUnique(data, canonicalize) {
  if (!canonicalize) {
    canonicalize = function(x) { return x; }
  }

  var lookup = {};

  return data.filter(function(item) {
     var serialised = canonicalize(item);

    if (lookup.hasOwnProperty(serialised)) {
      return false;
    } else { 
      lookup[serialised] = true;
      return true;
    }
  })
}
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • This answer works and looks fairly clean. However TimoSta's does work as well and is also fairly clean. So I took a measurement of the time and TimoSta's has more performance at about 0.368ms compared to this script that takes about 2.97ms on the same data. Not long either way but figured that would be a good way to check. The time difference in the long run is not bad so although I consider TimoSta's the best this is definitely a good resource. – Jordan Ramstad Oct 31 '16 at 17:41
  • Yeah, I imagine there are ways to make it faster. It's just illustrating the concept here. I'd definitely go with the more functional approach myself, mainly because the `.filter` is already there, so I don't need to write a separate loop and stuff. – VLAZ Oct 31 '16 at 17:46
2

The problem with Dates is that the operators === and !== don't work as expected (i.e. they compare pointers instead of actual values).

One solution is to use Underscore's uniq function with a custom transform function to compare the values:

var dates = data.map(d => {
   dp = new Date(Date.parse(d.date + '-' + d.year));
   dp.setFullYear(2000);
   return dp;
})

var unique = _.uniq(dates, false, function (date) {
   return date.getTime();
})
Maria Ines Parnisari
  • 16,584
  • 9
  • 85
  • 130
  • I would rather avoid adding underscore.js since I have not had a need for it anywhere else in my application yet, so I have not tested this answer. However if it does work I would say this is a fairly clean alternate if already using underscore.js – Jordan Ramstad Oct 31 '16 at 17:45
2

You can use Array.prototype.reduce() combined with Date.prototype.toString():

const dates = [
  new Date(2016, 09, 30, 10, 35, 40, 0), 
  new Date(2016, 09, 30, 10, 35, 40, 0), // same
  new Date(2016, 09, 30, 10, 35, 40, 0), // same
  new Date(1995, 07, 15, 03, 15, 05, 0)  // different
]
const uniqueDates = Object.values(
  dates.reduce((a, c) => (a[c.toString()] = c, a), {})
)

console.log(uniqueDates)
Yosvel Quintero
  • 18,669
  • 5
  • 37
  • 46
  • 1
    One should note that this takes advantage of JS's coercion of an object to a string, number, or symbol when used as a key to an object. If the language changes to allow object instances as keys to objects, this method won't work anymore... but hopefully that won't ever happen, as this is damned elegant. – fleebness Oct 28 '22 at 09:08
  • 1
    @fleebness I have added an improvement based on your comment.. Thank you – Yosvel Quintero Oct 28 '22 at 18:19
  • Ah, yep, that'd address that concern pretty nicely. – fleebness Oct 29 '22 at 20:27
1

I did mine like this (ES6+), based on some of the other answers:

const uniqueDates = [...new Set(dateInputArray.map(r => r.getTime()))].map((r: number)=>(new Date(r)));

Creates a unique set of dates converted to numbers using getTime(), then map them back to a Date object array afterwards.

tone
  • 1,374
  • 20
  • 47