Filter an array where obj[array[@]] is nil or empty

Viewed 246

Practising Ramda again.

So the situation is I have an obj:

const originalObj = {
  foo: "bar",
  std: "min",
  baz: "",
  key1: undefined,
  key2: "exit",
  key3: "val3",
  key4: "",
};

And I have an array that I know beforehand:

const toCheckArray = ["baz", "key1", "key2", "key3", "key4", "key5"];

For every element in the array, I need to check whether such element (as key) exists in the obj, and whether the corresponding value is nil/empty. If such key exists, and the value is non-zero/empty, then I make an HTTP call like this to update the value:

const findKey2AndUpdateObj = async (originalObj) => {
  const originalKey2 = originalObj.key2;
  const key2 = await remoteHttpCall(originalKey2);
  return { ...originalObj, key2: key2 };
};

For all the elements in the array, the remote HTTP call would be exactly the same, apart from the payload, of course.

My way of thing is to filter the list first, by doing the following steps:

  1. const hasArray = filter(has(__, originalObj), toCheckArray); this I believe will check whether the element as a prop exists in the target obj;
  2. I am trying to apply complement(anyPass([isNil, isEmpty])) to all the values of the obj and then somehow filter the corresponding key in the array;
  3. Iterate the array? to make API calls and then update the obj.

I guess what I am thinking is not the best way of doing it. Would love to hear your ideas! Also memorising the API call would be amazing too!


Or maybe I should flip step 1 and step 2? Filter out all the nil/empty ones from the obj and then do the has check.


I ended up doing this: filter(has(__, reject(anyPass([isEmpty, isNil]))(obj)), __)(arr). But surely there is better way.

Cheers!

4 Answers

Using a pipe, you make it a little more functional in the sense that you can pass the object into the pipe and output the keys, something like

pipe(reject(either(isNil, isEmpty)),keys,intersection(arr))(obj)

you could then pipe that into the api calls (with pipeWith)

I would use R.pick to get a partial object with only the requested keys, and then use R.reject to filter them further. Since the result is an object, you can use R.toPairs, and iterate the pairs to make api calls, and reconstruct the object with the new values.

const { curry, pipe, pick, reject, anyPass, isEmpty, isNil } = R;

const fn = curry((arr, obj) => pipe(
  pick(arr), 
  reject(anyPass([isEmpty, isNil])),
)(obj));

const toCheckArray = ["baz", "key1", "key2", "key3", "key4", "key5"];
const originalObj = {"foo":"bar","std":"min","baz":"","key2":"exit","key3":"val3","key4":""};

const result = fn(toCheckArray, originalObj);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Functional Programming is not necessarily about using Ramda, sometimes (for instance with async tasks), it might not help with readability and declarative code. I would use simple recursion here.

const asyncUpdater = async (keys, updater, data) => {
  // base case
  if(!keys.length) { return data };
  
  const [head, ...tail] = keys;
  const next = (d) => asyncUpdater(tail, updater, d);
  
  const value = data[head];
  
  // continue
  if(!value) { return next(data); }
  
  return next({ 
    ...data, 
    [head]: await updater(head, value)
  })
};

// ===
const keys = ["baz", "key1", "key2", "key3", "key4", "key5"];

const data = {
  foo: "bar",
  std: "min",
  baz: "",
  key1: undefined,
  key2: "exit",
  key3: "val3",
  key4: "",
};

function fakeHttp(key, value) {

  return Promise.resolve(`${value}__REMOTELY_UPDATED__`);
}

asyncUpdater(keys, fakeHttp, data).then(console.log);

I hope I understood your requirements correctly. Clean, self-documenting, and as functional as vanilla Javascript allows.

const resolveProperties = curry((fn, picklist, input) =>
  Promise.resolve(input)
    .then(pickAll(picklist))
    .then(reject(anyPass([isNil, isEmpty])))
    .then(map(fn))
    .then(Promise.props)
);

Usage and test-case:

import { pickAll, reject, isNil, map, anyPass, isEmpty, curry } from "ramda";
import { Promise } from "bluebird";

const mockHttp = (input) =>
  new Promise((res) => setTimeout(res(`${input} resolved`), 100));

const resolveProperties = curry((fn, picklist, input) =>
  Promise.resolve(input)
    .then(pickAll(picklist))
    .then(reject(anyPass([isNil, isEmpty])))
    .then(map(fn))
    .then(Promise.props)
);

test("resolveProperties", () => {
  const list = ["baz", "key1", "key2", "key3", "key4", "key5"];

  const input = {
    foo: "bar",
    std: "min",
    baz: "",
    key1: undefined,
    key2: "exit",
    key3: "val3",
    key4: "",
  };

  return resolveProperties(mockHttp, list, input).then((result) =>
    expect(result).toEqual({
      key2: "exit resolved",
      key3: "val3 resolved",
    })
  );
});
Related