Skip to content

Instantly share code, notes, and snippets.

@plukevdh
Last active April 11, 2022 13:56
Show Gist options
  • Save plukevdh/dec4b41d5b7d67f83be630afecee499e to your computer and use it in GitHub Desktop.
Save plukevdh/dec4b41d5b7d67f83be630afecee499e to your computer and use it in GitHub Desktop.
Ramda - Compact diff output for complex objects.
import R from 'ramda'
const isObject = R.compose(R.equals('Object'), R.type);
const allAreObjects = R.compose(R.all(isObject), R.values);
const hasLeft = R.has('left');
const hasRight = R.has('right');
const hasBoth = R.both(hasLeft, hasRight);
const isEqual = R.both(hasBoth, R.compose(R.apply(R.equals), R.values));
const markAdded = R.compose(R.append(undefined), R.values);
const markRemoved = R.compose(R.prepend(undefined), R.values);
const isAddition = R.both(hasLeft, R.complement(hasRight));
const isRemoval = R.both(R.complement(hasLeft), hasRight);
const objectDiff = R.curry(_diff);
function _diff(l, r) {
return R.compose(
R.map(R.cond([
[isAddition, markAdded],
[isRemoval, markRemoved],
[hasBoth, R.ifElse(
allAreObjects,
R.compose(R.apply(objectDiff), R.values),
R.values)
]
])),
R.reject(isEqual),
R.useWith(R.mergeWith(R.merge), [R.map(R.objOf('left')), R.map(R.objOf('right'))])
)(l, r);
}
export default objectDiff;
import objectDiff from 'objectDiff'
const lhs = {
one: 1,
two: [1,2],
three: { more: 'items', over: 'here' },
four: 'four',
five: 5,
};
const rhs = {
one: 1,
two: [1,3],
three: { more: 'robots', over: 'here' },
four: 4,
six: 6,
};
describe("deep diffing", () => {
const diff = objectDiff(lhs, rhs);
it('only detects changed', () => {
expect(diff).to.have.all.keys('two', 'three', 'four', 'five', 'six');
});
it('diffs arrays by returning the full array of items', () => {
expect(diff.two).to.eql([[1,2], [1,3]])
});
it('diffs objects by returning only changed items', () => {
expect(diff.three).to.eql({more: ['items', 'robots']});
});
it('detects plain value differences', () => {
expect(diff.four).to.eql(['four', 4])
});
it('detects removed values', () => {
expect(diff.five).to.eql([5, undefined])
});
it('detects added values', () => {
expect(diff.six).to.eql([undefined, 6])
})
});
@joeyfigaro
Copy link

Fantastic snippet—thank you for sharing.

Have you considered adding a method for returning true/false for determining if diffing has any results?

@jurStv
Copy link

jurStv commented Jun 23, 2018

Thanks!

@EvanTedesco
Copy link

EvanTedesco commented Oct 9, 2018

@joeyfigaro I have something that does what you are describing which uses this gist. Mine actually takes a whitelist of fields that you want to ignore but if you don't pass a whitelist(spec object) it will just return True or False. I have attached below:

const isNotNil = R.complement(R.isNil);

const isNotObject = R.complement(isObject);

const isArray = R.compose(
  R.equals('Array'),
  R.type,
);

const isObject = R.compose(
  R.equals('Object'),
  R.type,
);


// Prop :: String
// StructPath :: { [String]: Array<Prop> | StructPath }
// ExpandedStructPath :: { [String]: true | ExpandedStructPath }
// Spec :: StructPath | Array<Prop> | Prop | Null

// ExpandStructPath :: Spec -> ExpandedStructPath
const expandStructPath = spec =>
  R.cond([
    [
      R.is(String),
      R.compose(
        expandStructPath,
        R.of,
      ),
    ],
    [isObject, R.map(expandStructPath)],
    [isArray, R.converge(R.zipObj, [R.identity, R.identity])],
    [R.T, R.identity],
  ])(spec);

const _objDiffWhitelistFilter = R.curry((whiteList, diffRes) =>
  R.compose(
    R.reduce((acc, [key, diffResValue]) => {
      const whiteListValue = R.path([key], whiteList);

      // `null` `whiteList` indicates any diff is valid
      // Skip any indexes that don't exist in the whiteList
      if (isNotNil(whiteList) && R.isNil(whiteListValue)) {
        return acc;
      }

      // if: `diffResValue` is an array, indicates a valid difference
      // or: `whiteListValue` as a non object indicates it isn't spec'd as a substructure
      //     and there's no need to recurse any deeper.
      if (isArray(diffResValue) || isNotObject(whiteListValue)) {
        return R.reduced(true);
      }

      // Dig furtther down into the structure
      return _objDiffWhitelistFilter(whiteListValue, diffResValue);
    }, false),
    R.toPairs,
  )(diffRes),
);

// ObjDiffWhitelistFilter :: Spec -> ObjDiffResult -> Boolean
const objDiffWhitelistFilter = R.curry((whiteListStruct, objDiffRes) =>
  R.compose(
    _objDiffWhitelistFilter(R.__, objDiffRes),
    expandStructPath,
  )(whiteListStruct),
);

// NOTES:
// - When `WhiteListStruct` is `Nil`, assume all fields should be checked

// isObjDiff :: Spec -> Object -> Object -> Boolean
const isObjDiff = R.curry((whiteListStruct, left, right) =>
  R.compose(
    objDiffWhitelistFilter(whiteListStruct),
    objDiff,
  )(left, right),
);

Also some test cases to see what the spec/whitelist object looks like:

import isObjectDiff from './isObjectDiff';

const objectOne = {
  foo: {
    variantInfo: {
      product: 'ONEAPP_SAMTV',
      somethingExtra: 'hi mom',
      featureOverrides: [
        { name: 'bug', type: 'STRING', value: 'feature' },
        { name: 'extra', type: 'STRING', value: 'feature' },
      ],
      jsonForOverrides: 'A',
      useJsonForOverrides: false,
    },
  },
};
const objectTwo = {
  foo: {
    variantInfo: {
      product: 'ONEAPP_SAMTV',
      featureOverrides: [{ name: 'bug', type: 'STRING', value: 'feature' }],
      jsonForOverrides: '',
      useJsonForOverrides: false,
    },
  },
};

describe('is Object diff', () => {
  it('ignores extraneous parameters', () => {
    const objectDiffStruct = {
      foo: { variantInfo: ['useJsonForOverrides', 'product'] },
    };
    expect(isObjectDiff(objectDiffStruct)(objectOne, objectTwo)).toBe(false);
  });

  it('takes an array at the top of the structure', () => {
    const objectDiffStruct = ['foo'];
    expect(isObjectDiff(objectDiffStruct)(objectOne, objectTwo)).toBe(true);
  });

  it('defaults to all keys', () => {
    const objectDiffStruct = null;
    expect(isObjectDiff(objectDiffStruct)(objectOne, objectTwo)).toBe(true);
  });
});

It also supports more complex whitelist/spec objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment