nkint
nkint

Reputation: 11733

Translate this code in functional javascript (aggregate 2 array in a map)

I've just read about reactive programming and I am enthusiast about it. So I decided to revise my skill on functional programming. I don't know if this is the right place.

I have two array, one of tags and one of tasks that contains tags. I'd like to aggregate the two and go out with a tasksByTagName. I've tried to use lodash but I didn't managed to do it in a readable way so I'm posting what I've done with normal for statements.

More over I'm interested in understanding how to think in a flow based way, such as thinking my aggregate function as a transformation between two stream, as the reactive programming article linked above suggest.

So, let's start with datas:

var tags = [ 
  { id: 1, name: 'Week 26' },
  { id: 2, name: 'week 27' },
  { id: 3, name: 'Week 25' },
  { id: 4, name: 'Week 25' }
];

var tasks = [
  {
    "name": "bar",
    "completed": false,
    "tags": [
      { "id": 1 },
      { "id": 2 }
    ]
  },
  {
    "name": "foo",
    "completed": true,
    "tags": [
      { "id": 1 }
    ]
  },
  {
    "name": "dudee",
    "completed": true,
    "tags": [
      { "id": 3 },
      { "id": 1 },
      { "id": 4 }
    ]
  }
];

And this is my piece of code:

var _ = require('lodash');

function aggregate1(tasks, tags) {

  var tasksByTags = {};
  for(var t in tasks) {
    var task = tasks[t];
    for(var i=0; i<task.tags.length; ++i) {
      var tag = task.tags[i];
      if( !tasksByTags.hasOwnProperty(tag.id) ) {
        tasksByTags[tag.id] = [];  
      }
      tasksByTags[tag.id].push(task.name);
    }
  }

  var tagById = {};
  for(var t in tags) {
    var tag = tags[t];
    tagById[tag.id] = tag.name;
  }

  var tasksByTagsName = _.mapKeys(tasksByTags, function(v, k) {
    return tagById[k];
  })

  return tasksByTagsName;
}

module.exports.aggregate1 = aggregate1;

For completeness, this is also the test with test data:

var tags = [ 
  { id: 1, name: 'Week 26' },
  { id: 2, name: 'week 27' },
  { id: 3, name: 'Week 25' },
  { id: 4, name: 'Week 25' }
];

var tasks = [
  {
    "name": "bar",
    "completed": false,
    "tags": [
      { "id": 1 },
      { "id": 2 }
    ]
  },
  {
    "name": "foo",
    "completed": true,
    "tags": [
      { "id": 1 }
    ]
  },
  {
    "name": "dudee",
    "completed": true,
    "tags": [
      { "id": 3 },
      { "id": 1 },
      { "id": 4 }
    ]
  }
];

var goodResults1 = {
  'Week 26': [ 'bar', 'foo', 'dudee' ],
  'week 27': [ 'bar' ],
  'Week 25': [ 'dudee' ] 
};    



var assert = require('assert')

var aggregate1 = require('../aggregate').aggregate1;

describe('Aggegate', function(){
  describe('aggregate1', function(){
    it('should work as expected', function(){
      var result = aggregate1(tasks, tags);
      assert.deepEqual(goodResults1, result);
    })
  })
})

Upvotes: 1

Views: 84

Answers (2)

ryeballar
ryeballar

Reputation: 30098

This approach below translates to an object reduction from the tags object. With the help of lazy evaluation, the process below only runs tags.length x tasks.length times. If you're unfamiliar with lazy evaluation, here's a reference.

Example Here

script.js

function agregateTasks(tasks, tags) {

  return _.reduce(tags, function(result, tag) {

    // chain tasks to lazy evaluate
    result[tag.name] = _(tasks) 
      // get all tasks with tags that contains tag.id
      .filter({ tags: [_.pick(tag, 'id')]}) 
      // get all names
      .pluck('name') 
      // concatenate existing task names if there are
      .concat(result[tag.name] || []) 
      // only unique task name values
      .uniq() 
      // run lazy evaluation
      .value(); 

    return result;

  }, {});

}

script.spec.js

describe('aggregate()', function(){

  var tags, tasks, expectedResults;

  beforeEach(function() {
    tags = [ 
      { id: 1, name: 'Week 26' },
      { id: 2, name: 'week 27' },
      { id: 3, name: 'Week 25' },
      { id: 4, name: 'Week 25' }
    ];

    tasks = [
      {
        "name": "bar",
        "completed": false,
        "tags": [
          { "id": 1 },
          { "id": 2 }
        ]
      },
      {
        "name": "foo",
        "completed": true,
        "tags": [
          { "id": 1 }
        ]
      },
      {
        "name": "dudee",
        "completed": true,
        "tags": [
          { "id": 3 },
          { "id": 1 },
          { "id": 4 }
        ]
      }
    ];

    expectedResults = {
      'Week 26': [ 'bar', 'foo', 'dudee' ],
      'week 27': [ 'bar' ],
      'Week 25': [ 'dudee' ] 
    };

  });

  it('should work as expected', function() {

    var result = aggregateTasks(tasks, tags);
    expect(result).toEqual(expectedResults);

  });

});

Upvotes: 2

nrabinowitz
nrabinowitz

Reputation: 55678

Here's one approach: https://jsfiddle.net/ok6wf7eb/

function aggregate(tasks, tags) {

    // make an id: tag map
    var tagMap = tags.reduce(function(map, tag) {
        map[tag.id] = tag.name;
        return map;
    }, {});

    // collect tasks by tag
    return tasks.reduce(function(map, task) {
        // loop through task tags
        task.tags.forEach(function(tag) {
            // get tag name by id
            var tagId = tagMap[tag.id];
            // get array, making it on the fly if needed
            var tasks = map[tagId] || (map[tagId] = []);
            // add the new task name
            tasks.push(task.name);
        });
        return map;
    }, {});

}

This uses reduce as a slightly more "functional" approach to the problem, though for legibility I'm never certain there's much gain over

var collector = {};
items.forEach(function(item) {
    // add to collector
});

Upvotes: 1

Related Questions