Jordan Ramstad
Jordan Ramstad

Reputation: 179

Unique Array for dates Javascript

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.

Upvotes: 14

Views: 18698

Answers (6)

Yosvel Quintero
Yosvel Quintero

Reputation: 19070

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)

Upvotes: 2

tone
tone

Reputation: 1434

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.

Upvotes: 2

Sergey Reutskiy
Sergey Reutskiy

Reputation: 2811

ES6 way:

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

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

Upvotes: 23

VLAZ
VLAZ

Reputation: 28970

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;
    }
  })
}

Upvotes: 6

Maria Ines Parnisari
Maria Ines Parnisari

Reputation: 17486

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();
})

Upvotes: 2

TimoStaudinger
TimoStaudinger

Reputation: 42460

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.

Upvotes: 12

Related Questions