JSON diff в Javascript [дубликат]

Предупреждающее сообщение верное. Вы возвращаете адрес локального массива, который исчезает после возвращения функции.

Вы можете сделать это, используя распределение динамической памяти:

char *recvmsg(){
    char *buffer = (char*)malloc(1024);
    return buffer;
}

Уловка заключается в том, что вам нужно убедитесь, что вы free() указатель позже, чтобы избежать утечки памяти.

В качестве альтернативы вы можете передать буфер в функцию.

void recvmsg(char *buffer,int buffer_size){
    //  write to buffer
}

void main(){
    char buffer[1024];
    recvmsg(buffer,1024);
}

Это позволяет избежать необходимости выделение памяти. Это действительно предпочтительный способ сделать это.

153
задан Martin Jespersen 21 December 2011 в 02:03
поделиться

13 ответов

Я написал небольшой класс, который делает то, что вы хотите, вы можете проверить его здесь .

Единственное, что отличается от вашего предложения, это то, что я не считаю [1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]] будет таким же, потому что я думаю, что массивы не равны, если порядок их элементов не одинаковый. Конечно, это может быть изменено, если необходимо. Кроме того, этот код может быть дополнительно расширен, чтобы использовать функцию в качестве аргумента, которая будет использоваться для форматирования объекта diff произвольным образом на основе переданных примитивных значений (теперь это задание выполняется методом «compareValues»).

var deepDiffMapper = function() {
    return {
        VALUE_CREATED: 'created',
        VALUE_UPDATED: 'updated',
        VALUE_DELETED: 'deleted',
        VALUE_UNCHANGED: 'unchanged',
        map: function(obj1, obj2) {
            if (this.isFunction(obj1) || this.isFunction(obj2)) {
                throw 'Invalid argument. Function given, object expected.';
            }
            if (this.isValue(obj1) || this.isValue(obj2)) {
                return {
                    type: this.compareValues(obj1, obj2),
                    data: (obj1 === undefined) ? obj2 : obj1
                };
            }

            var diff = {};
            for (var key in obj1) {
                if (this.isFunction(obj1[key])) {
                    continue;
                }

                var value2 = undefined;
                if ('undefined' != typeof(obj2[key])) {
                    value2 = obj2[key];
                }

                diff[key] = this.map(obj1[key], value2);
            }
            for (var key in obj2) {
                if (this.isFunction(obj2[key]) || ('undefined' != typeof(diff[key]))) {
                    continue;
                }

                diff[key] = this.map(undefined, obj2[key]);
            }

            return diff;

        },
        compareValues: function(value1, value2) {
            if (value1 === value2) {
                return this.VALUE_UNCHANGED;
            }
            if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
                return this.VALUE_UNCHANGED;
            }
            if ('undefined' == typeof(value1)) {
                return this.VALUE_CREATED;
            }
            if ('undefined' == typeof(value2)) {
                return this.VALUE_DELETED;
            }

            return this.VALUE_UPDATED;
        },
        isFunction: function(obj) {
            return {}.toString.apply(obj) === '[object Function]';
        },
        isArray: function(obj) {
            return {}.toString.apply(obj) === '[object Array]';
        },
        isDate: function(obj) {
            return {}.toString.apply(obj) === '[object Date]';
        },
        isObject: function(obj) {
            return {}.toString.apply(obj) === '[object Object]';
        },
        isValue: function(obj) {
            return !this.isObject(obj) && !this.isArray(obj);
        }
    }
}();


var result = deepDiffMapper.map({
      a:'i am unchanged',
      b:'i am deleted',
      e:{ a: 1,b:false, c: null},
      f: [1,{a: 'same',b:[{a:'same'},{d: 'delete'}]}],
      g: new Date('2017.11.25')
  },
  {
      a:'i am unchanged',
      c:'i am created',
      e:{ a: '1', b: '', d:'created'},
      f: [{a: 'same',b:[{a:'same'},{c: 'create'}]},1],
      g: new Date('2017.11.25')
  });
console.log(result);
90
ответ дан sbgoran 18 August 2018 в 17:58
поделиться
  • 1
    +1 Это не плохой кусок кода. Однако есть ошибка (проверьте этот пример: jsfiddle.net/kySNu/3 c создается как undefined, но должен быть строкой 'i am created'), и, кроме того, делать то, что мне нужно, так как ему не хватает глубокого сравнения значений массива, который является наиболее важной (и сложной / сложной) частью. В качестве побочной заметки конструкция 'array' != typeof(obj) бесполезна, поскольку массивы являются объектами, которые являются экземплярами массивов. – Martin Jespersen 21 December 2011 в 23:43
  • 2
    Я обновил код, но я не уверен, какое значение вы хотите в результирующем объекте, теперь код возвращает значение из первого объекта, а если он не существует, то значение из второго будет установлено как данные. – sbgoran 22 December 2011 в 00:18
  • 3
    И как вы имеете в виду «отсутствие сравнения значений глубоких массивов»? для массивов вы получите для каждого индекса объект {type: ..., data:..}. То, что отсутствует, - это поиск значения из первого массива во втором, но, как я упоминал в своем ответе, я не думаю, что массивы равны, если порядок их значений не равен ([1, 2, 3] is not equal to [3, 2, 1], на мой взгляд). – sbgoran 22 December 2011 в 00:32
  • 4
    @MartinJespersen ОК, как бы вы в целом относились к этим массивам: [{key: 'value1'}] and [{key: 'value2'}, {key: 'value3'}]. Теперь первый объект в первом массиве, обновленный с «значением1», или "значение2". И это простой пример, он может усложниться с глубоким вложением. Если вы хотите / нуждаетесь в глубоком сопоставлении вложенности, независимо от положения ключа, не создавайте массивы объектов, создавайте объекты с вложенными объектами, как в предыдущем примере: {inner: {key: 'value1'}} and {inner: {key: 'value2'}, otherInner: {key: 'value3'}}. – sbgoran 22 December 2011 в 10:27
  • 5
    Я согласен с вами в последней точке зрения - исходная структура данных должна быть заменена на то, что легче сделать с фактической разницей. Поздравляю, ты прибил ее :) – Martin Jespersen 2 March 2014 в 20:39

Я знаю, что опаздываю на вечеринку, но мне нужно было что-то подобное, что вышеприведенные ответы не помогли.

Я использовал функцию часовых часов Angular для обнаружения изменений в переменной. Мне не только нужно было знать, изменилось ли свойство в переменной, но я также хотел убедиться, что измененное свойство не было временным, рассчитанным полем. Другими словами, я хотел игнорировать некоторые свойства.

Вот код: https://jsfiddle.net/rv01x6jo/

Вот как использовать it:

// To only return the difference
var difference = diff(newValue, oldValue);  

// To exclude certain properties
var difference = diff(newValue, oldValue, [newValue.prop1, newValue.prop2, newValue.prop3]);

Надеюсь, это кому-то поможет.

-1
ответ дан a11smiles 18 August 2018 в 17:58
поделиться
  • 1
    Пожалуйста, введите код в свой ответ, а не только скрипку. – xpy 4 May 2017 в 09:11
  • 2
    Похоже, что defineProperty решит эту проблему с лучшей производительностью, если я правильно помню, что она работает вплоть до IE9. – Peter 6 April 2018 в 09:12

Я использовал этот фрагмент кода для выполнения описанной вами задачи:

function mergeRecursive(obj1, obj2) {
    for (var p in obj2) {
        try {
            if(obj2[p].constructor == Object) {
                obj1[p] = mergeRecursive(obj1[p], obj2[p]);
            }
            // Property in destination object set; update its value.
            else if (Ext.isArray(obj2[p])) {
                // obj1[p] = [];
                if (obj2[p].length < 1) {
                    obj1[p] = obj2[p];
                }
                else {
                    obj1[p] = mergeRecursive(obj1[p], obj2[p]);
                }

            }else{
                obj1[p] = obj2[p];
            }
        } catch (e) {
            // Property in destination object not set; create it and set its value.
            obj1[p] = obj2[p];
        }
    }
    return obj1;
}

это даст вам новый объект, который объединит все изменения между старым объектом и новым объектом из ваша форма

2
ответ дан AMember 18 August 2018 в 17:58
поделиться
  • 1
    Я использую среду Ext здесь, но вы можете ее заменить и использовать любую другую фреймворк, который вам нужен ... – AMember 21 December 2011 в 17:37
  • 2
    Слияние объектов тривиально и может быть выполнено так же легко, как $.extend(true,obj1,obj2) с помощью jQuery. Это совсем не то, что мне нужно. Мне нужна разница между двумя объектами, а не их комбинацией. – Martin Jespersen 21 December 2011 в 20:03
  • 3
    извините ... Я пропустил точку, должен лучше читать в следующий раз :( – AMember 22 December 2011 в 09:12
  • 4
    его большое, что Ext используется здесь – peroxide 11 March 2015 в 12:29

Вот библиотека JavaScript, которую вы можете использовать для поиска diff между двумя объектами JavaScript:

Github url: https://github.com/cosmicanant/recursive-diff

Npmjs url: https://www.npmjs.com/package/recursive-diff

recursiveDiff [/g2]

Вы можете использовать библиотеку recursive-diff в браузере, а также Node.js. Для браузера выполните следующие действия:

<script type="text" src="index.js"/>
<script type="text/javascript">
     var ob1 = {a:1, b: [2,3]};
     var ob2 = {a:2, b: [3,3,1]};
     var delta = diff.getDiff(ob1,ob2); 
     /* console.log(delta) will dump following data 
     {
         '/a':{operation:'update',value:2}
         '/b/0':{operation:'update',value:3},
         '/b/2':{operation:'add',value:1},
     } */
     var ob3 = diff.applyDiff(ob1, delta); //expect ob3 is deep equal to ob2
 </script>

Если в node.js вам может потребоваться модуль «рекурсивный-diff» и использовать его, как показано ниже:

var diff = require('recursive-diff');
var ob1 = {a: 1}, ob2: {b:2};
var diff = diff.getDiff(ob1, ob2);
8
ответ дан Anant 18 August 2018 в 17:58
поделиться
  • 1
    Например, это не будет учитывать изменения в свойствах даты. – trollkotze 17 November 2017 в 11:03

В наши дни для этого доступно немало модулей. Недавно я написал модуль для этого, потому что меня не устраивало множество различных модулей, которые я нашел. Его называют odiff: https://github.com/Tixit/odiff . Я также перечислил кучу самых популярных модулей и почему они не были приемлемы в readme из odiff, которые вы могли бы просмотреть, если odiff не обладает требуемыми свойствами. Вот пример:

var a = [{a:1,b:2,c:3},              {x:1,y: 2, z:3},              {w:9,q:8,r:7}]
var b = [{a:1,b:2,c:3},{t:4,y:5,u:6},{x:1,y:'3',z:3},{t:9,y:9,u:9},{w:9,q:8,r:7}]

var diffs = odiff(a,b)

/* diffs now contains:
[{type: 'add', path:[], index: 2, vals: [{t:9,y:9,u:9}]},
 {type: 'set', path:[1,'y'], val: '3'},
 {type: 'add', path:[], index: 1, vals: [{t:4,y:5,u:6}]}
]
*/
6
ответ дан B T 18 August 2018 в 17:58
поделиться

Использование Underscore, простой diff:

var o1 = {a: 1, b: 2, c: 2},
    o2 = {a: 2, b: 1, c: 2};

_.omit(o1, function(v,k) { return o2[k] === v; })

Результаты в частях o1, которые соответствуют, но с разными значениями в o2:

{a: 1, b: 2}

It 'd различны для глубокого diff:

function diff(a,b) {
    var r = {};
    _.each(a, function(v,k) {
        if(b[k] === v) return;
        // but what if it returns an empty object? still attach?
        r[k] = _.isObject(v)
                ? _.diff(v, b[k])
                : v
            ;
        });
    return r;
}

Как указано в комментариях @Juhana, вышесказанное является только разложением a -> b, а не обратимым (что означает, что дополнительные свойства в b будут игнорироваться). Вместо этого используйте a -> b -> a:

(function(_) {
  function deepDiff(a, b, r) {
    _.each(a, function(v, k) {
      // already checked this or equal...
      if (r.hasOwnProperty(k) || b[k] === v) return;
      // but what if it returns an empty object? still attach?
      r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
    });
  }

  /* the function */
  _.mixin({
    diff: function(a, b) {
      var r = {};
      deepDiff(a, b, r);
      deepDiff(b, a, r);
      return r;
    }
  });
})(_.noConflict());

См. http://jsfiddle.net/drzaus/9g5qoxwj/ для полного примера + тесты + mixins

69
ответ дан drzaus 18 August 2018 в 17:58
поделиться
  • 1
    Не уверен, почему вы получили вниз, это было достаточно, поскольку вы предоставили неглубокий простой пример, а также более сложную глубокую функцию. – Seiyria 1 November 2014 в 02:03
  • 2
    @Seiyria ненавистников ненавидит, я думаю ... Я сделал оба, потому что изначально думал, что omit будет глубоким различием, но был неправильным, поэтому включен и для сравнения. – drzaus 3 November 2014 в 20:33
  • 3
    Хорошее решение. Я бы предложил изменить r[k] = ... : v в r[k] = ... : {'a':v, 'b':b[k] }, таким образом вы можете увидеть два значения. – guyaloni 3 February 2015 в 18:59
  • 4
    Оба они возвращают ложный отрицательный результат, когда объекты в противном случае идентичны, а во втором - больше элементов, например. {a:1, b:2} и {a:1, b:2, c:3}. – JJJ 19 January 2016 в 21:53
  • 5
    Это должно быть _.omitBy вместо _.omit. – J P 23 February 2017 в 13:11

Я просто использую ramda, для решения той же проблемы, мне нужно знать, что изменилось в новом объекте. Итак, вот мой дизайн.

const oldState = {id:'170',name:'Ivab',secondName:'Ivanov',weight:45};
const newState = {id:'170',name:'Ivanko',secondName:'Ivanov',age:29};

const keysObj1 = R.keys(newState)

const filterFunc = key => {
  const value = R.eqProps(key,oldState,newState)
  return {[key]:value}
}

const result = R.map(filterFunc, keysObj1)

Результат - имя свойства и его статус.

[{"id":true}, {"name":false}, {"secondName":true}, {"age":false}]
1
ответ дан Ivan Titkov 18 August 2018 в 17:58
поделиться

Я разработал функцию с именем «compareValue ()» в Javascript. он возвращает, является ли значение одинаковым или нет. Я вызвал compareValue () для цикла одного объекта. вы можете получить различие двух объектов в diffParams.

var diffParams = {};
var obj1 = {"a":"1", "b":"2", "c":[{"key":"3"}]},
    obj2 = {"a":"1", "b":"66", "c":[{"key":"55"}]};

for( var p in obj1 ){
  if ( !compareValue(obj1[p], obj2[p]) ){
    diffParams[p] = obj1[p];
  }
}

function compareValue(val1, val2){
  var isSame = true;
  for ( var p in val1 ) {

    if (typeof(val1[p]) === "object"){
      var objectValue1 = val1[p],
          objectValue2 = val2[p];
      for( var value in objectValue1 ){
        isSame = compareValue(objectValue1[value], objectValue2[value]);
        if( isSame === false ){
          return false;
        }
      }
    }else{
      if(val1 !== val2){
        isSame = false;
      }
    }
  }
  return isSame;
}
console.log(diffParams);

1
ответ дан jarangseo 18 August 2018 в 17:58
поделиться

Я уже написал функцию для одного из моих проектов, который будет сравнивать объект как пользовательские параметры с его внутренним клоном. Он также может проверять и даже заменять значения по умолчанию, если пользователь вводил плохой тип данных или удалялся в чистом javascript.

В IE8 работает 100%. Протестировано успешно.

//  ObjectKey: ["DataType, DefaultValue"]
reference = { 
    a : ["string", 'Defaul value for "a"'],
    b : ["number", 300],
    c : ["boolean", true],
    d : {
        da : ["boolean", true],
        db : ["string", 'Defaul value for "db"'],
        dc : {
            dca : ["number", 200],
            dcb : ["string", 'Default value for "dcb"'],
            dcc : ["number", 500],
            dcd : ["boolean", true]
      },
      dce : ["string", 'Default value for "dce"'],
    },
    e : ["number", 200],
    f : ["boolean", 0],
    g : ["", 'This is an internal extra parameter']
};

userOptions = { 
    a : 999, //Only string allowed
  //b : ["number", 400], //User missed this parameter
    c: "Hi", //Only lower case or case insitive in quotes true/false allowed.
    d : {
        da : false,
        db : "HelloWorld",
        dc : {
            dca : 10,
            dcb : "My String", //Space is not allowed for ID attr
            dcc: "3thString", //Should not start with numbers
            dcd : false
      },
      dce: "ANOTHER STRING",
    },
    e: 40,
    f: true,
};


function compare(ref, obj) {

    var validation = {
        number: function (defaultValue, userValue) {
          if(/^[0-9]+$/.test(userValue))
            return userValue;
          else return defaultValue;
        },
        string: function (defaultValue, userValue) {
          if(/^[a-z][a-z0-9-_.:]{1,51}[^-_.:]$/i.test(userValue)) //This Regex is validating HTML tag "ID" attributes
            return userValue;
          else return defaultValue;
        },
        boolean: function (defaultValue, userValue) {
          if (typeof userValue === 'boolean')
            return userValue;
          else return defaultValue;
        }
    };

    for (var key in ref)
        if (obj[key] && obj[key].constructor && obj[key].constructor === Object)
          ref[key] = compare(ref[key], obj[key]);
        else if(obj.hasOwnProperty(key))
          ref[key] = validation[ref[key][0]](ref[key][1], obj[key]); //or without validation on user enties => ref[key] = obj[key]
        else ref[key] = ref[key][1];
    return ref;
}

//console.log(
    alert(JSON.stringify( compare(reference, userOptions),null,2 ))
//);

/ * result

{
  "a": "Defaul value for \"a\"",
  "b": 300,
  "c": true,
  "d": {
    "da": false,
    "db": "Defaul value for \"db\"",
    "dc": {
      "dca": 10,
      "dcb": "Default value for \"dcb\"",
      "dcc": 500,
      "dcd": false
    },
    "dce": "Default value for \"dce\""
  },
  "e": 40,
  "f": true,
  "g": "This is an internal extra parameter"
}

*/
0
ответ дан Samad Aghaei 18 August 2018 в 17:58
поделиться

Я хотел бы предложить решение ES6 ... Это односторонний diff, что означает, что он вернет ключи / значения из o2, которые не идентичны их аналогам в o1:

let o1 = {
  one: 1,
  two: 2,
  three: 3
}

let o2 = {
  two: 2,
  three: 3,
  four: 4
}

let diff = Object.keys(o2).reduce((diff, key) => {
  if (o1[key] === o2[key]) return diff
  return {
    ...diff,
    [key]: o2[key]
  }
}, {})
20
ответ дан senornestor 18 August 2018 в 17:58
поделиться
  • 1
    Хорошее решение, но вы можете проверить, что if(o1[key] === o1[key]) – bm_i 13 June 2016 в 09:02
  • 2
    Хороший улов – senornestor 13 June 2016 в 18:11
  • 3
    Закончен ли код? Я получаю Uncaught SyntaxError: Unexpected token ... – Seano 11 May 2017 в 10:33
  • 4
    Он работает для меня на консоли Chrome 58. Ключевое слово let - ES6, а оператор расширения объекта ... - это будущее ES, поэтому вам нужно запустить его в среде, которая их поддерживает. – senornestor 12 May 2017 в 19:25
  • 5
    – Ulysse BN 18 July 2017 в 16:09
  • 6
    Да, это не рекурсивный @Spurious – Nemesarial 14 August 2017 в 11:12

с использованием lodash.

function difference(object, base) {
            function changes(object, base) {
                return _.transform(object, function(result, value, key) {
                    if (!_.isEqual(value, base[key])) {
                        result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
                    }
                });
            }
            return changes(object, base);
        }
0
ответ дан STEEL 18 August 2018 в 17:58
поделиться

Более расширенная и упрощенная функция из ответа sbgoran. Это позволяет осуществлять глубокое сканирование и находить симлимутность массива.

var result = objectDifference({
      a:'i am unchanged',
      b:'i am deleted',
      e: {a: 1,b:false, c: null},
      f: [1,{a: 'same',b:[{a:'same'},{d: 'delete'}]}],
      g: new Date('2017.11.25'),
      h: [1,2,3,4,5]
  },
  {
      a:'i am unchanged',
      c:'i am created',
      e: {a: '1', b: '', d:'created'},
      f: [{a: 'same',b:[{a:'same'},{c: 'create'}]},1],
      g: new Date('2017.11.25'),
      h: [4,5,6,7,8]
  });
console.log(result);

function objectDifference(obj1, obj2){
    if((dataType(obj1) !== 'array' && dataType(obj1) !== 'object') || (dataType(obj2) !== 'array' && dataType(obj2) !== 'object')){
        var type = '';

        if(obj1 === obj2 || (dataType(obj1) === 'date' && dataType(obj2) === 'date' && obj1.getTime() === obj2.getTime()))
            type = 'unchanged';
        else if(dataType(obj1) === 'undefined')
            type = 'created';
        if(dataType(obj2) === 'undefined')
            type = 'deleted';
        else if(type === '') type = 'updated';

        return {
            type: type,
            data:(obj1 === undefined) ? obj2 : obj1
        };
    }
  
    if(dataType(obj1) === 'array' && dataType(obj2) === 'array'){
        var diff = [];
        obj1.sort(); obj2.sort();
        for(var i = 0; i < obj2.length; i++){
            var type = obj1.indexOf(obj2[i]) === -1?'created':'unchanged';
            if(type === 'created' && (dataType(obj2[i]) === 'array' || dataType(obj2[i]) === 'object')){
                diff.push(
                    objectDifference(obj1[i], obj2[i])
                );
                continue;
            }
            diff.push({
                type: type,
                data: obj2[i]
            });
        }

        for(var i = 0; i < obj1.length; i++){
            if(obj2.indexOf(obj1[i]) !== -1 || dataType(obj1[i]) === 'array' || dataType(obj1[i]) === 'object')
                continue;
            diff.push({
                type: 'deleted',
                data: obj1[i]
            });
        }
    } else {
        var diff = {};
        var key = Object.keys(obj1);
        for(var i = 0; i < key.length; i++){
            var value2 = undefined;
            if(dataType(obj2[key[i]]) !== 'undefined')
                value2 = obj2[key[i]];

            diff[key[i]] = objectDifference(obj1[key[i]], value2);
        }

        var key = Object.keys(obj2);
        for(var i = 0; i < key.length; i++){
            if(dataType(diff[key[i]]) !== 'undefined')
                continue;

            diff[key[i]] = objectDifference(undefined, obj2[key[i]]);
        }
    }

    return diff;
}

function dataType(data){
    if(data === undefined || data === null) return 'undefined';
    if(data.constructor === String) return 'string';
    if(data.constructor === Array) return 'array';
    if(data.constructor === Object) return 'object';
    if(data.constructor === Number) return 'number';
    if(data.constructor === Boolean) return 'boolean';
    if(data.constructor === Function) return 'function';
    if(data.constructor === Date) return 'date';
    if(data.constructor === RegExp) return 'regex';
    return 'unknown';
}

0
ответ дан StefansArya 18 August 2018 в 17:58
поделиться

Использование Lodash:

_.mergeWith(oldObj, newObj, function (objectValue, sourceValue, key, object, source) {
    if ( !(_.isEqual(objectValue, sourceValue)) && (Object(objectValue) !== objectValue)) {
        console.log(key + "\n    Expected: " + sourceValue + "\n    Actual: " + objectValue);
    }
});

Я не использую ключ / объект / источник, но я оставил его там, если вам нужно доступ к ним. Сравнение объектов просто препятствует консоли печатать различия с консолью от внешнего элемента до самого внутреннего элемента.

Вы можете добавить некоторую логику внутри для обработки массивов. Возможно, сначала отсортируйте массивы. Это очень гибкое решение.

EDIT

Изменено с _.merge на _.mergeWith из-за обновления lodash. Спасибо Aviron за то, что заметили изменения.

14
ответ дан toshiomagic 18 August 2018 в 17:58
поделиться
  • 1
    Не пренебрегайте мной, не предоставляя обратной связи. Я довольно новичок, и чем больше людей, тем меньше я пытаюсь внести свой вклад в этот сайт. – toshiomagic 29 June 2015 в 14:10
  • 2
    В lodash 4.15.0 _.merge с функцией customizer больше не поддерживается, поэтому вместо этого следует использовать _.mergeWith. – Aviran Cohen 28 August 2016 в 15:21
Другие вопросы по тегам:

Похожие вопросы: