二つのJSON、差分を出したかった。

ソフトウェア,技術

この記事は2018年6月13日に書かれたものです。ご注意ください。

ちょっと自分の趣味&お仕事の関係で、以下のようなJSONを取得する機会がよくあった(もちろん以下のは適当な例)。

[
  {"id":"01", "name":"山田", "obj":"old", "data":"重複"},
  {"id":"02", "name":"野田", "obj":"old", "data":""},
  {"id":"04", "name":"森岡", "obj":"old", "data":"重複"},
  {"id":"05", "name":"稲田", "obj":"old", "data":"重複"},
  {"id":"09", "name":"杉山", "obj":"old", "data":""},
  {"id":"10", "name":"益子", "obj":"old", "data":"重複"}
]

次のタイミングでJSONを取得すると、データが追加されたり削除されてたりする。

[
  {"id":"01", "name":"山田", "obj":"new", "data":"重複"},
  {"id":"03", "name":"神山", "obj":"new", "data":""},
  {"id":"04", "name":"森岡", "obj":"new", "data":"重複"},
  {"id":"05", "name":"稲田", "obj":"new", "data":"重複"},
  {"id":"06", "name":"関谷", "obj":"new", "data":""},
  {"id":"07", "name":"寺島", "obj":"new", "data":""},
  {"id":"08", "name":"浅川", "obj":"new", "data":""},
  {"id":"10", "name":"益子", "obj":"new", "data":"重複"}
]

まぁよくありがちだけど、この差分を出したい(oldから削除され、newで追加されたもの)と思った。ほんと急な思いつきだった。

悩んだ結果テキトーにJavaScriptを書いた。DOM操作してないのでjQueryは未使用。
なお、今回差分を出すにあたって「ID」があるか無いかしか見ていないので、汎用性には乏しいかと思う。(nameやdataなどが変わってても同一として見なしちゃうからね。その辺は適当に弄ればなんとかなるはず)

テキトーなコード

var oldObj = [
  {"id":"01", "name":"山田", "obj":"old", "data":"重複"}, // 0
  {"id":"02", "name":"野田", "obj":"old", "data":""},
  {"id":"04", "name":"森岡", "obj":"old", "data":"重複"}, // 2
  {"id":"05", "name":"稲田", "obj":"old", "data":"重複"}, // 3
  {"id":"09", "name":"杉山", "obj":"old", "data":""},
  {"id":"10", "name":"益子", "obj":"old", "data":"重複"} // 5
];
var newObj = [
  {"id":"01", "name":"山田", "obj":"new", "data":"重複"}, // 0
  {"id":"03", "name":"神山", "obj":"new", "data":""},
  {"id":"04", "name":"森岡", "obj":"new", "data":"重複"}, // 2
  {"id":"05", "name":"稲田", "obj":"new", "data":"重複"}, // 3
  {"id":"06", "name":"関谷", "obj":"new", "data":""},
  {"id":"07", "name":"寺島", "obj":"new", "data":""},
  {"id":"08", "name":"浅川", "obj":"new", "data":""},
  {"id":"10", "name":"益子", "obj":"new", "data":"重複"} // 7
];
var res = objectDiff(oldObj, newObj);
console.log('--旧--');
console.log(oldObj);
console.log('--新--');
console.log(newObj);
console.log('--結果--');
console.log(res);

function objectDiff(objA, objB) {
  // 重複している箇所を配列へ保存
  var ASpl = [];
  var BSpl = [];
  for (var n = 0, m = objA.length; n < m; n++) {
    var ATrg = objA[n].id;
    for (var i = 0, j = objB.length; i < j; i++) {
      var BTrg = objB[i].id;
      if (ATrg == BTrg) {
        //console.log(`一致ID:${ATrg} / objA(n):${n}番目 / objB(i):${i}番目`);
        ASpl.push(n);
        BSpl.push(i);
      }
    }
  }
  //console.log(`ASpl:${ASpl} / BSpl:${BSpl}`);
  // objをコピーし、重複した箇所を後ろから削除する
  var objA_diff = objA.slice();
  var objB_diff = objB.slice();
  diffObj(objA_diff, ASpl);
  diffObj(objB_diff, BSpl);
  function diffObj(obj, spl) {
    for (var n = spl.length - 1; n >= 0; n--) obj.splice(spl[n], 1);
  }
  // objから重複した箇所を抜き出す
  var objA_lap = overlapObj(objA, ASpl);
  var objB_lap = overlapObj(objB, BSpl);
  function overlapObj(obj, spl) {
    var ary = []
    for (var n = 0, m = spl.length; n < m; n++) {
      var num = spl[n];
      ary.push(obj[num]);
    }
    return ary;
  }
  // オブジェクトを作り返す
  var obj = {
    "old": {"diff": objA_diff, "lap": objA_lap},
    "new": {"diff": objB_diff, "lap": objB_lap}
  };
  return obj;
}

結果は以下のような感じになります(Chromeのconsoleで)

テキトーなかいせつ

var oldObj = [
  {"id":"01", "name":"山田", "obj":"old", "data":"重複"}, // 0
  {"id":"02", "name":"野田", "obj":"old", "data":""},
  {"id":"04", "name":"森岡", "obj":"old", "data":"重複"}, // 2
  {"id":"05", "name":"稲田", "obj":"old", "data":"重複"}, // 3
  {"id":"09", "name":"杉山", "obj":"old", "data":""},
  {"id":"10", "name":"益子", "obj":"old", "data":"重複"} // 5
];
var newObj = [
  {"id":"01", "name":"山田", "obj":"new", "data":"重複"}, // 0
  {"id":"03", "name":"神山", "obj":"new", "data":""},
  {"id":"04", "name":"森岡", "obj":"new", "data":"重複"}, // 2
  {"id":"05", "name":"稲田", "obj":"new", "data":"重複"}, // 3
  {"id":"06", "name":"関谷", "obj":"new", "data":""},
  {"id":"07", "name":"寺島", "obj":"new", "data":""},
  {"id":"08", "name":"浅川", "obj":"new", "data":""},
  {"id":"10", "name":"益子", "obj":"new", "data":"重複"} // 7
];
var res = objectDiff(oldObj, newObj);

特に語ることはない。
配列を変数へ入れvar res = objectDiff(oldObj, newObj);の関数で返り値を待つだけ。
コメントアウトしてる// 0は配列の何番目が重複しているのかメモってるだけです。後(というか次)で出てきます。

function objectDiff(objA, objB) {
  // 重複している箇所を配列へ保存
  var ASpl = [];
  var BSpl = [];
  for (var n = 0, m = objA.length; n < m; n++) {
    var ATrg = objA[n].id;
    for (var i = 0, j = objB.length; i < j; i++) {
      var BTrg = objB[i].id;
      if (ATrg == BTrg) {
        //console.log(`一致ID:${ATrg} / objA(n):${n}番目 / objB(i):${i}番目`);
        ASpl.push(n);
        BSpl.push(i);
      }
    }
  }
  //console.log(`ASpl:${ASpl} / BSpl:${BSpl}`);

var res = objectDiff(oldObj, newObj);で送られてきたobjAとobjBの重複している箇所を配列で取得しています。
forでobjAの1つめを見るとき、objBをforで全てを回して一致しているかを確認。あれば重複してる箇所(index)を配列へ保存…って感じです。
forとforを入れ子にしているので「objAの数×objBの数」分だけ試行されるので正直ヤバい。30件程度であれば30*30の900回で済むのでさくっと終わるけど、モノによっては500件と500件あるので…あへぇ。(もうjsじゃない方が良いのでは…?わかりませんが!!!)
ちなconsole.log(`一致ID:${ATrg} / objA(n):${n}番目 / objB(i):${i}番目`);のコメントアウトを外すことで「一致したID&それがobjA・objBの何番目か」がログに出されます。

一致ID:01 / objA(n):0番目 / objB(i):0番目
一致ID:04 / objA(n):2番目 / objB(i):2番目
一致ID:05 / objA(n):3番目 / objB(i):3番目
一致ID:10 / objA(n):5番目 / objB(i):7番目

またconsole.log(`ASpl:${ASpl} / BSpl:${BSpl}`);のコメントアウトを外すと

ASpl:0,2,3,5 / BSpl:0,2,3,7

と配列の中身を見ることが出来ます(オブジェクトの状態で見たければconsole.log(ASpl);とかに書き換えてください)
うーん、log()の説明いらないな??

  // objをコピーし、重複した箇所を後ろから削除する
  var objA_diff = objA.slice();
  var objB_diff = objB.slice();
  diffObj(objA_diff, ASpl);
  diffObj(objB_diff, BSpl);
  function diffObj(obj, spl) {
    for (var n = spl.length - 1; n >= 0; n--) obj.splice(spl[n], 1);
  }

objAとobjBをslice()を利用してコピーします。
なぜコピーするかって?obj.splice(spl[n], 1);を行うと、元々存在したobjAとobjBが破壊されるからです…(そうすると後々困るので、、、)
コピーした「objA_diffとobjB_diff」に対して「ASplとBSpl」を利用して、1つずつ重複した配列を削除していきます。なお、配列の削除は必ず後ろから行います。(前からやると順番ズレるぞ!!私はそれで失敗したぞ!!!

  // objから重複した箇所を抜き出す
  var objA_lap = overlapObj(objA, ASpl);
  var objB_lap = overlapObj(objB, BSpl);
  function overlapObj(obj, spl) {
    var ary = []
    for (var n = 0, m = spl.length; n < m; n++) {
      var num = spl[n];
      ary.push(obj[num]);
    }
    return ary;
  }

今度は逆に重複した箇所を抜き出します。まぁ抜き出すと言っても、重複した配列を新しい配列へpush()してそれを返す。ってだけすけど。

  // オブジェクトを作り返す
  var obj = {
    "old": {"diff": objA_diff, "lap": objA_lap},
    "new": {"diff": objB_diff, "lap": objB_lap}
  };
  return obj;

そして最期に「旧データの差分・重複」と「新データの差分・重複」を配列にして返す。

以上です。
オッシャァ!!!

objectDiff(objA, objB)objectDiff(objA, objB ,trg)とかにして(trgはString)、どこを重複のキーにするか指定させてあげると汎用性高まるのでは?(っつても最初に書いた例のようなJSONじゃないと無意味かと…)
「差分」といってもヒトによって異なるから、その時々に合わせて書き換えていくしか無いっすね。

というかJavaScript以外でやった方がスピード速いのでは????()