1

I've searched SO for a while trying to find an answer to this question as I imagine someone's run into this scenario. Please point me in the right direction if an answer is already out there. I have an array of objects containing books. These books are stored in a database and can have one or many writers. For each writer in the writers table, there is a column for book_id(FK), writer_name, and review_id(FK). Therefore if there are multiple writers on a book, there is a row for each writer containing the same information except the name. Here is an example of a JSON response from the DB:

var data = [
{"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
{"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
{"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis", "review_id" : 5},
{"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
{"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
{"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
{"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
{"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];

I would like to filter this array so that I end up with 3 objects, one for each book, that has no repeat values and each writer concatenated as a comma delimited string. Like so:

var data = [
{"id": 1, "writer" : "John Joseph Doe, Daniel Smith, Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis, Jill Steinberg, Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams, Mary Joe Williams, Helen Andrews, Samantha Jones", "review_id" : 3}
];

So I made an attempt to create a function that uses a for loop and a 'first' variable to keep track of which object I'm at based on it's id. My logic is to add each writer to the first instance of each object with a unique id, then remove all the proceeding objects with the same id after the writer has been added. Therefore I'm left with a single object for each book id and a concatenated string of writers separated by commas. The results are close to what I'm looking for, but I can't get it right. I'm not sure if removing the objects from the array is my issue or if I'm taking the entirely wrong approach in general. Here's a working fiddle: http://jsfiddle.net/mlabita37/Lvc0u55v/8987/ . There is a textarea above the table that I'm using as a 'console' to show the final data array, just expand to see the full result.

Here's some snippets of that code:

HTML:

<div ng-controller="BookCtrl">
<table class="table table-striped">
 <thead>
  <tr>
    <th>Book ID</th>
    <th>Writer</th>
    <th>Review ID</th>
  </tr>
</thead>
<tbody>
  <tr ng-repeat="book in books">
    <td>{{ book.id }}</td>
    <td>{{ book.writer }}</td>
    <td>{{ book.review_id }}</td>
    <textarea>{{ books }}</textarea>
  </tr>
</tbody>

Controller:

var data = [
{"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
{"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
{"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis", "review_id" : 5},
{"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
{"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
{"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
{"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
{"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];
combine = function(data){
  var first = data[0];
  for(var i = 1; i < data.length; i++){
    if(data[i].id === first.id){
      first.writer = first.writer + ", " + data[i].writer;
      data.splice(i, 1);
    }
    if(data[i].id !== first.id){
      first = data[i];
      var j = i+1;
      if(data[j].id === first.id){
        first.writer = first.writer + ", " + data[j].writer;
        data.splice(j, 1);
      }
   }
 }  
};
combine(data);
$scope.books = data;

Another idea I had was to use a filter, however I don't have much experience with them and don't know if that would be a better approach. I also wasn't sure if this was something that could/should be handled in my MySQL query instead, or in the backend PHP. I'm under the impression that this is best handled on the front end.

1
  • Your combine function wrong. I wrote a correct one. You can even make it a Angular filter Commented Sep 1, 2016 at 5:37

2 Answers 2

1

var data = [
{"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
{"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
{"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis", "review_id" : 5},
{"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
{"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
{"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
{"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
{"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];

function existsAt(array, key, value) {
    for (var i = 0; i < array.length; i++) {
        if (array[i][key] == value) {
            return i;
        }
    }
    return false;
}

var _data = [];
_data.push(data[0]);

for (var i = 1; i < data.length; i++) {
    var alreadyExistsAt = existsAt(_data, 'id', data[i].id);
    if (alreadyExistsAt !== false) {
        _data[alreadyExistsAt].writer += ', ' + data[i].writer;
    } else {
        _data.push(data[i]);
    }
}

document.body.innerHTML = JSON.stringify(_data);

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much, this was perfect! I ended up setting $scope.books equal to the new _data array to achieve the desired result. I like how the objects could contain as many properties as you'd like and it still works perfectly. Great approach, it's exactly how I envisioned it working but my logic was flawed. Thanks again!
1

I hope the variable names in the code make it fairly self-explanatary.

  • The outer forEach loop selects each review (i.e. currReview) in turn from the complete list of reviews.
  • The inner forEach loop loops over each pre-existing concatenated book review object (i.e. prevReviewsForThisBook...empty at first). This inner loop determines whether the current review is for a book that already has at least one review for it:
    • If it is, then the writer property of that pre-existing review object is updated to include the new writer.
    • If it is not, then the entire current review object is pushed onto the array of pre-existing book review objects.

Note that your id which is presumably your book_id is always identical to your review_id. Is that really what you want? It seems like one of them is either redundant/unnecessary, or one of them is calculated/determined incorrectly. The latter seems possible. For example, are the review_id of the review done by John Joseph Doe and the review_id of the review done by Daniel Smith both supposed to have the same value, 4? I would think that those are separate reviews and thus should have different review_id values, even if they both have the same id/book_id value (because those two people both reviewed the same book). This issue is not central to answering this question, but you might want to re-think that.

var data = allReviews = [
  {"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
  {"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
  {"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
  {"id": 2, "writer" : "Erin Davis", "review_id" : 5},
  {"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
  {"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
  {"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
  {"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
  {"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
  {"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];
var reviewsByBook = [];
allReviews.forEach(function(currReview) {
  var currReviewIsForNewBook = true;
  reviewsByBook.forEach(function(prevReviewsForThisBook) {
    if (currReview.id === prevReviewsForThisBook.id) {
      prevReviewsForThisBook.writer += ", " + currReview.writer;
      currReviewIsForNewBook = false;
    }
  });
  if (currReviewIsForNewBook) {
    reviewsByBook.push(currReview);
 }
});
console.log(reviewsByBook);

1 Comment

Thank you for the great answer, this works great but was a bit harder for me to adapt to my existing code. I think the focus on the reviews, not the merge, made it tougher to follow. In regards to your note, this was a just a hypothetical example that I made in order to explain the problem I needed to solve. But for clarity, I regarded the reviews as another table and their id's corresponds to a description, ie. One Star, Two Stars, Three Stars, Four Stars, Five Stars. Regardless, thank you very much for this answer, it's works great as well and provides a really good alternate approach!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.