Source: derive.js

import { isarrayofobjects, isgeojson } from "./helpers/helpers.js";

/**
 * @function derive
 * @summary Add a field to a dataset. The function allows to add a new property
 * @param {object|array} data - A GeoJSON FeatureCollection or an array of objects
 * @param {object} options - Optional parameters
 * @param {string} [options.key = "_newkey"] - Name of the property
 * @param {number|string|function} [options.value] - You can set a simple number or string. You can also specify a function like `d=> d.properties.gdp/d.properties.pop * 1000`. With the *, +, - and / operators, you can also directly write `“gdp/pop*100”`.
 * @param {boolean} [options.mutate = false] - Use `true` to update the input data. With false, you create a new object, but the input object remains the same.
 * @returns {object|array} -  A GeoJSON FeatureCollection or an array of objects. (it depends on what you've set as `data`).
 * @example
 * geotoolbox.derive(*a geojson or an array of objects*, {key: "gdppc", value:"gdp/pop"})
 */
export function derive(data, { key = "_newkey", value, mutate = false } = {}) {
  // deep copy ?
  let x = data;
  const operators = /[+\-/*]/;

  // geojson
  if (isgeojson(data)) {
    if (!mutate) {
      x = JSON.parse(JSON.stringify(data));
    }
    const n = x.features.length;

    if (
      typeof value == "number" ||
      (typeof value == "string" && !operators.test(value))
    ) {
      x.features.forEach((d) => {
        d.properties[key] = value;
      });
    } else if (typeof value == "function") {
      const values = x.features.map(value);
      x.features.forEach((d, i) => {
        d.properties[key] = values[i];
      });
    } else if (typeof value == "string" && operators.test(value)) {
      const prop = [
        ...new Set(
          x.features
            .map((d) => d.properties)
            .map((d) => Object.keys(d))
            .flat()
        ),
      ];
      const newprop = prop.map((d) => "d.properties['" + d + "']");
      const functrsing =
        "d => " +
        prop.reduce(
          (acc, mot, i) => acc.replace(new RegExp(mot, "g"), newprop[i]),
          value
        );
      const values = x.features.map(eval(functrsing));
      x.features.forEach((d, i) => {
        d.properties[key] = values[i];
      });
    }
  }

  // Array of objects
  else if (isarrayofobjects(data)) {
    const n = x.length;

    if (
      typeof value == "number" ||
      (typeof value == "string" && !operators.test(value))
    ) {
      x.forEach((d) => {
        d[key] = value;
      });
    } else if (typeof value == "function") {
      const values = x.map(value);
      x.forEach((d, i) => {
        d[key] = values[i];
      });
    } else if (typeof value == "string" && operators.test(value)) {
      const prop = [...new Set(x.map((d) => Object.keys(d)).flat())];
      const newprop = prop.map((d) => "d['" + d + "']");

      const functrsing =
        "d => " +
        prop.reduce(
          (acc, mot, i) => acc.replace(new RegExp(mot, "g"), newprop[i]),
          value
        );
      const values = x.map(eval(functrsing));

      x.forEach((d, i) => {
        d[key] = values[i];
      });
    }

    if (mutate) {
      data.splice(0, data.length, ...x);
    }
  }

  return x;
}