1

I want to 'reduce' the array to only max values for each x (or index 0) value in a JavaScript multidimensional array.

My Array is as follows:

var mulitple = [["1/2013", 1],
                ["1/2013", 5],
                ["1/2013", 7],
                ["1/2013", 6],
                ["1/2013", 5],
                ["2/2013", 7],
                ["2/2013", 10],
                ["2/2013", 10],
                ["3/2013", 7],
                ["3/2013", 10],
                ["3/2013", 10],
                ["4/2013", 1],
                ["4/2013", 5],
                ["4/2013", 7],
                ["4/2013", 6],
                ["4/2013", 5],
                ["5/2013", 7]];

So the final result should be as follows:

[["1/2013", 7],
 ["2/2013", 10],
 ["3/2013", 10],
 ["4/2013", 7],
 ["5/2013", 7]];

How can I achieve this in JavaScript.

EDIT:

Aww man who voted my question down.

Anyhow, this is what I have come up with.

var max = 0;
var newarray = [];
for (var i = 1; i < mulitple.length; i++) {
    if (mulitple[i - 1][0] == mulitple[i][0]) {
        if (mulitple[i - 1][1] > max) {
            max = mulitple[i - 1][1];
        }
    }
    else {
        if (mulitple[i - 1][1] > max) {
            max = mulitple[i - 1][1];
        }
        newarray.push([mulitple[i - 1][0], max]);
        max = 0;
    }
}
newarray.push([mulitple[mulitple.length - 1][0], max]);

The problem that I am having is that I can't get that last value (for the lone record) to get in the array. This was my result after I ran the code above.

[["1/2013", 7], ["2/2013", 10], ["3/2013", 10], ["4/2013", 7], ["5/2013", 0]]
progrAmmar
  • 2,606
  • 4
  • 29
  • 58
  • Are those first values meant to be parsed as month/year dates or as math expressions? And, are you using the second value to break ties? – jfriend00 Feb 13 '15 at 02:44
  • It would involve a loop, a second array, and capturing the items that have the max value for each group.... What have you tried? Are you having some specific problem other than "solve this whole thing for me"? – TLS Feb 13 '15 at 02:44
  • This can give you some tips: http://stackoverflow.com/questions/14446511/what-is-the-most-efficient-method-to-groupby-on-a-javascript-array-of-objects – Aguardientico Feb 13 '15 at 02:46
  • @jfriend00 the first values are month and year NOT math expressions – progrAmmar Feb 13 '15 at 02:47
  • @TLS I understand much of array operations in JavaScript, to be honest I don't know how to go on about this. Just a simple nudge might help me solve this. – progrAmmar Feb 13 '15 at 02:50
  • fellows, please check the edited question – progrAmmar Feb 13 '15 at 03:04
  • Per your edit, it never has a chance to figure out the max of the last item because there's nothing else to compare it to. Instead of building a new item for the new array, just grab the item that has the max value. See my answer as a suggestion. – TLS Feb 13 '15 at 03:12

3 Answers3

2

This assumes that original array is already sorted. If not, you will have to write additional function to sort out.

function findMaximums(data) {
    var out = [], maximums = {}, order = new Set;

    data.reduce(function(acc, pair) {
        if (
            // Accumulator has value and it's lower than current
            (acc[pair[0]] && acc[pair[0]][1] < pair[1]) ||
            // Accumulator doesn't have value
            !acc[pair[0]]
        ) {
            acc[pair[0]] = pair; // Store maximum in accumulator
            order.add(pair[0]) // Store order in set
        }
        return acc;
    }, maximums);

    order.forEach(function(key) {
        out.push(maximums[key]); // Populate out with maximums by order
    });

    return out;
}

findMaximums(multiple);

/*[
    [
        "1/2013",
        7
    ],
    [
        "2/2013",
        10
    ],
    [
        "3/2013",
        10
    ],
    [
        "4/2013",
        7
    ],
    [
        "5/2013",
        7
    ]
]*/

Update 1: same, but without Set.

function findMaximums(data) {
    var order = [];

    var maximums = data.reduce(function(acc, pair) {
        if (
            // Accumulator has value and it's lower than current
            (acc[pair[0]] && acc[pair[0]][2] < pair[1]) ||
            // Accumulator doesn't have value
            !acc[pair[0]]
        ) {
            // Store maximum
            acc[pair[0]] = pair;
            // Store order
            if (order.indexOf(pair[0]) === -1) {
                order.push(pair[0])
            }
        }
        return acc;
    }, {});

    return order.map(function(key) {
        return maximums[key]; // Populate out with maximums by order
    });
}

Update 2: Shorter version.

function findMaximums(data) {
  return data.filter(function(p1, i1) {
    return !data.some(function(p2, i2) {
      return p1[0] === p2[0] && ( (p1[1] < p2[1]) || (p1[1] === p2[1] && i1 > i2) );
    });
  });
}

In this version I let pair to remain in output if there are no other pairs in input data that:

  1. Have same month.
  2. Have bigger value.

or

  1. Have same value, but occur earlier than tested pair. This prevents duplicates.

Read here more about used Array methods: filter, some.

  • Well, look at that! JavaScript has a `reduce` method! – TLS Feb 13 '15 at 03:28
  • It should be said that this uses the `Set` object who's use either requires careful consideration of browser support (IE 11 or greater) or a polyfill. FYI, a set polyfill is [here](http://stackoverflow.com/questions/7958292/mimicking-sets-in-javascript/7958422#7958422). – jfriend00 Feb 13 '15 at 03:33
  • I don't see the output getting sorted into month/year order? Does this assume the input is already sorted and that order is retained? Does the `Set` object guarantee to preserve order? Or does this not guarantee any particular output order? – jfriend00 Feb 13 '15 at 03:37
  • I tried this [here](http://jsfiddle.net/jfriend00/unw4wgou/) and it does not sort the output by `month/year` (not sure if that was a requirement, but thought I'd mention it). In Chrome, it appears to return the data in the order encountered in the original array. – jfriend00 Feb 13 '15 at 03:41
  • @progrAmmar didn't mention anything about order, so I've made an assumption about it right at the start. – Klaster_1 Нет войне Feb 13 '15 at 03:52
  • @progrAmmar - are you OK with requiring IE11+? – jfriend00 Feb 13 '15 at 04:15
  • @jfriend00 actually I am making an app that can work on all browsers – progrAmmar Feb 13 '15 at 04:49
  • @progrAmmar - do you understand that the use of the `Set` object in this answer requires IE 11+ and will not work in older versions of IE without changing this code or adding other code? – jfriend00 Feb 13 '15 at 05:15
  • I've added version without Set. – Klaster_1 Нет войне Feb 13 '15 at 06:16
  • I used the without Set version :) – progrAmmar Feb 13 '15 at 09:01
1

Assuming the array as defined in the original question, which is sorted to have each grouping together...

Completely untested code:

var reduced = [];
var groupName = '';
var groupMax;
var groupIndex;
var l = multiple.length; // Grab the array length only once

for (var i = 0; i < l; i++){
    // Current Group Name doesn't match last Group Name
    if (multiple[i][0] !== groupName) {
        // Last Group Name is not empty (it's not the first Group)
        if (groupName !== '') {
            // Assume groupIndex has been set and grab the item
            reduced.push(multiple[groupIndex]);
        }
        // Grab the new Group Name and set the initial Max and Index
        groupName = multiple[i][0];
        groupMax = multiple[i][1];
        groupIndex = i;
    }

    // Current Value is bigger than last captured Group Max
    if (multiple[i][1] > groupMax) {
        // Grab the new Group Max and the current Index
        groupMax = multiple[i][1];
        groupIndex = i;
    }
}

// Grab the last index, since there's no new group after the last item
reduced.push(multiple[groupIndex]);

There could be some syntax or logic errors. I didn't actually run this code, but I think the concept is correct.

TLS
  • 3,090
  • 2
  • 25
  • 33
1

Here's a tested version using a map to collect all the unique values, then the output is sorted by month/year and is independent of the order of the input data. This also works in all browsers (IE6+).

Working demo: http://jsfiddle.net/jfriend00/dk1tc73s/

function findLargest(list) {
    var map = {}, output = [], item, key, val, current;
    for (var i = 0; i < list.length; i++) {
        item = list[i];
        key = item[0];
        val = item[1];
        current = map[key];
        if (current) {
            // this date is in the map, so make sure to save the largest
            // value for this date
            if (val > current) {
                map[key] = val;
            }            
        } else {
            // this date is not yet in the map, so add it
             map[key] = val;
        }
    }
    // map contains all the largest values, output it to an array
    // the map is not in a guaranteed order
    for (var key in map) {
        output.push([key, map[key]])
    }

    // actually parse to numbers in sort so the comparison
    // works properly on number strings of different lengths
    function parseDate(str) {
        var split = str.split("/");
        return {m: +split[0], y: +split[1]};
    }
    // now sort the output
    output.sort(function(t1, t2) {
        var diffYear, a, b;
        a = parseDate(t1[0]);
        b = parseDate(t2[0]);
        diffYear = a.y - b.y;
        if (diffYear !== 0) {
            return diffYear;
        } else {
            return a.m - b.m;
        }
    });
    return output;
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979