3
\$\begingroup\$

I implemented a method, to create an object based on a series of strings that are passed to it, of any depth or length. The way I implemented the solution was to map over the arguments. Assuming that strings are passed in the form x.a, it then splits the string by the '.', returning just a single object with that key if the array only had one element otherwise, it was passed through a reducer to produce an array of objects which would then be merged into one object.

I'd like to make this better/ are there any concerns with using a reducer in a map function, since it's on the order of O(n2)?

type TKeys = string[]
type TTarget = {
  [key: string]: TTarget | string,
}
type TTemplate = {
  target: TTarget,
  props: ReadonlyArray<string>
}
interface ITemplate {
  target: {
    [key: string]: string | TTemplate["target"]
  },
  props: ReadonlyArray<string>
}
const createObject = <T>(...keys: TKeys): T => (
  keys.map(key => {
    const props = key.split('.');
    const reducerTemplate: ITemplate = {
      target: {},
      props: ['']
    }

    return props.length < 2
      ? {
          [key]: ''
        }
      : props.reduce((obj: ITemplate, prop: string) => {
          const target: ITemplate["target"] = obj.target
          if (obj.props.length === 0) {
            console.log(obj, prop);
            obj.props = [prop];
            target[prop] = {};
            return obj;
          }
          const object: ITemplate["target"] = obj
            .props
            .reduce((prev: TTemplate["target"] , next: string) => (target[next] || prev[next]), {})
          const atEnd: boolean = props.indexOf(prop) === props.length - 1
          object[prop] = atEnd
            ? ''
            : {}
          obj.props = [
            ...obj.props,
            prop
          ];

          return atEnd
            ? target
            : obj;
        }, reducerTemplate)
  }).reduce((main: {} | TTarget, obj: TTarget ): TTarget => ({
    ...main,
    ...obj
}), {}))

Example Usage:

createObject('a.b.c.d', 'hello.world', 'test') 
//outputs
    {
    a: {
        b: {
            c: {
                d: '',
            },
        },
    },
    hello: {
        world: '',
    },
    test: '',
}
createObject('hello')
//outputs
{ hello: '' }
createObject('a.b')
//outputs
{
    a: {
        b: '',
    },
};
\$\endgroup\$
1
  • \$\begingroup\$ Typescript chokes on this as soon as I paste it into any playground I'm familiar with, even ignoring the compiler errors, the code itself doesn't seem to work. Cannot set property 'a' of undefined given your first example. \$\endgroup\$
    – Gerrit0
    Jun 6, 2018 at 1:25

2 Answers 2

2
\$\begingroup\$

In general case this always maps to O(n2). You may introduce some complex optimisations but this yields to long hardly readable code. Are you sure you will benefit from this?

I am a huge fan of CoffeeScript language and both CoffeeScript and TypeScript do a lot of hidden work so it is crucial to see the compiled JavaScript. Also, if your primary concearn is perfomance you will benefit greatly from plain JavaScript solution.

So here is my attempt on it. It is simular to Máté Safranka solution (which is quite nice and readable). It uses Array.prototype.reduce to remove all local variables in favor of function arguments:

function createObject(...pathes) {
  return pathes.reduce(function (obj, path) {
    path.split('.').reduce((obj, key) => obj[key] = obj[key] || {}, obj);
    return obj;
  }, {});
}

// Some tests
console.log(createObject('a.b.c.d', 'hello.world', 'test'));
console.log(createObject('a', 'a.b', 'a.b.c'));
console.log(createObject());
console.log(createObject('')); // note: not handled properly

The last one is not handled properly, but to me it looks more like an invalid input.

Tests

I've added provided solutions to jsperf test and compiled your TypeScript at Playground but for some reason it doesn't work as intended.

So, here is the benchmark. FYI check results in different browsers.

\$\endgroup\$
0
\$\begingroup\$

I think a plain loop works better here. I don't know TypeScript unfortunately, but I'm pretty sure you can adapt this easily enough:

function createObject(...keys) {
    let obj = {};
    for (let key of keys) {
        let putInto = obj;
        let tokens = key.split('.');
        for (let i = 0; i < tokens.length; i++) {
            let name = tokens[i];
            let value = (i === tokens.length - 1) ? '' : {};
            putInto[name] = putInto[name] || value;
            putInto = putInto[name];
        }
    }
    return obj;
}
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.