Как динамически заполнить / развернуть 2d массив, используя функцию обратного вызова в Ramda.js - PullRequest
0 голосов
/ 13 октября 2019

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

Изложение проблемы

Я хотел бы создать функциюкак это

finalFunction({ array, header, ...args }, callbackFunctionToTransformArray)

Ограничения

  • Данный массив всегда является двумерным массивом
  • Заголовок предоставляется в видестрока для передачи в callbackFunction
  • Функция обратного вызова всегда должна возвращать объект «изменения», содержащий заголовки в качестве ключей. Значения для каждого ключа содержат массив значений для вставки

, который может передавать все три сценария при следующих заданных входных параметрах (часть объекта ввода):

{
 array = [
  ["#","FirstName","LastName"]
  ["1","tim","foo"],
  ["2","kim","bar"]
],
header: "FirstName",
...args
}

Важно

Проблемы заключаются не в создании функций обратного вызова, а скорее в создании "finalFunction".

Сценарий 1: Преобразование существующего массива без расширения

// return for the second row of the array
callback1 => {
  changes: {
    FirstName: ["Tim"]
  }
};
// return for the third row of the array
callback1 => {
  changes: {
    FirstName: ["Kim"]
  }
};

finalFunction({ array, header, ...args }, callback1) 

должно вернуть

{
  array: [
  ["#","FirstName","LastName"]
  ["1","Tim","foo"],
  ["2","Kim","bar"]
  ],
  header: "FirstName",
  ...args
}

Сценарий 2: Преобразование существующего массива с горизонтальным расширением

// return given for the second row
callback2 => {
  changes: {
    FullName: ["Tim Foo"]
  }
};
// return given for the third row
callback2 => {
  changes: {
    FullName: ["Kim Bar"]
  }
};

finalFunction({ array, header, ...args }, callback2) 

следуетreturn

{
  array: [
  ["#","FirstName","LastName","FullName"]
  ["1","Tim","foo","Tim Foo"],
  ["2","Kim","bar","Kim Bar"]
  ],
  header: "FirstName",
  ...args
}

Сценарий 3: Преобразование существующего массива с вертикальным и горизонтальным расширением

// return given for the second row
callback3 => {
  changes: {
    "Email": ["tim.foo@stackoverflow.com","timmy@gmail.com"],
    "MailType": ["Work","Personal"]
  }
};
// return given for the third row
callback3 => {
  changes: {
    "Email": ["kim.bar@stackoverflow.com","kimmy@aol.com"],
    "MailType": ["Work","Personal"]
  }
};

finalFunction({ array, header, ...args }, callback3) 

должно вернуть

{
  array: [
  ["#","FirstName","LastName","Email","MailType"]
  ["1","Tim","foo","tim.foo@stackoverflow.com","Work"],
  ["1","Tim","foo","timmy@gmail.com","Personal"],
  ["2","Kim","bar","kim.bar@stackoverflow.com","Work"],
  ["2","Kim","bar","kimmy@aol.com","Personal"]
  ],
  header: "FirstName",
  ...args
}

Текущий прогресс

TheЗамечательный @Scott Sauyet помог мне создать функцию слияния двухмерного массива и объекта изменений:

const addInputToArray = ({ array, changes, ...rest}) => ({
  array: Object .entries (changes) .reduce ((a, [k, vs], _, __, index = array [0] .indexOf (k)) =>
    vs.reduce(
      (a, v, i) =>
        (i + 1) in a
          ? update ((i + 1), update (index, v, a [i + 1] ), a)
          : concat (a, [update (index, v, map (always (''), array [0]) )] ),
      a),
    array
  ),
  ...rest
})

Это прекрасно работает для сценария № 1. Однако я не могу заставить это решение автоматически создавать заголовки, если они не являются частью исходного массива.

Однако я добился прогресса в вертикальном расширении, описанном в сценарии 3.

const expandVertically = ({ array, header, index = array[0].indexOf(header), ...args }, callback) => ({
      array: array.reduce((a, v, i) => {
        if (i === 0) {
          a.push(v);
        } else {
          const arrayBlock = R.repeat(v, callback(v[index]).length);
          arrayBlock.unshift(array[0]);
          const result = addInputToArray({
            changes: callback(v[index]).changes,
            array: arrayBlock
          }).array;
          result.shift();
          result.map(x => a.push(x));
        }
        return a;
      }, []),
      header,
      ...args
    })

На мой взгляд, вновь созданная логика должна будет.

  1. Вызвать функцию обратного вызова, чтобы получить записи, которые могут отсутствовать для первой строки заголовка
  2. Добавитьотсутствующие ключи объекта «изменения» в строке заголовка
  3. Сокращение по сравнению с массивом, пропускающим первую строку
  4. Всегда предполагайте, что блок массива (это нормально, если блок массива имеет только длину, чтосценарии покрытия # 1 и # 2)
  5. Убедитесь, что длина блока массива не нуждается в параметре "length", который должен быть предоставлен обратным вызовом, а скорее будет получена из массива значений, предоставленных для каждого ключа в "Изменения "obj

Текущие проблемы

  1. Текущее решение вертикального расширения требует обратного вызова, чтобы предоставить" lпараметр ength "в его результате, чтобы получить правильное количество повторений для каждой строки источника.
  2. Текущая функция для объединения" изменений "с sourceArray не создает автоматически новые заголовки, если они не могут быть найденыв первой строке исходного массива.

Я чувствую, что это выполнимо, и это даст большие преимущества текущему проекту, над которым я работаю, так как он применяет стандартизированный интерфейс для всех заполнений массивов/ разложения.

Однако я чувствую себя застрявшим, особенно в том, как охватить все 3 сценария в одной функции.

Любые идеи или идеи будут с благодарностью.

Ответы [ 2 ]

2 голосов
/ 18 октября 2019

Основываясь на замечательном вкладе Скотта, я хотел поделиться версией этой функциональности, в которой не используется flatMap, а вместо этого работает Ramda (что обеспечивает большую поддержку среды.

const R = require('ramda')

// Helper function
const transposeObj = (obj, len = Object.values(obj)[0].length) =>
  [...Array(len)].map((_, i) => Object.entries(obj).reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}));

// Main function
const finalFunction = (
  { array: [headers, ...rows], ...rest },
  callback,
  changes = rows.map(r => transposeObj(callback(r).changes)),
  allHeaders = R.flatten([
    ...headers,
    R.chain(t => R.chain(Object.keys, t), [...changes])
      .filter(k => !headers.includes(k))
      .filter((x, i, a) => a.indexOf(x) == i)
  ])
) => {
  const resultRows = R.chain(
    (row, i = R.indexOf(row, [...rows])) =>
      changes[i].map(change =>
        Object.entries(change).reduce(
          (r, [k, v]) => [...r.slice(0, allHeaders.indexOf(k)), v, ...r.slice(allHeaders.indexOf(k) + 1)],
          row.slice(0)
        )
      ),
    [...rows]
  );
  return {
    array: [allHeaders, ...resultRows],
    ...rest
  };
};
2 голосов
/ 14 октября 2019

Вот одна попытка. Возможно, я все еще что-то здесь упускаю, потому что я полностью игнорирую ваш параметр header. Это как-то необходимо, или эта функциональность теперь захвачена ключами в change объектах, сгенерированных вашими функциями обратного вызова?

// Helper function
const transposeObj = (obj, len = Object .values (obj) [0] .length) => 
  [... Array (len)] .map (
    (_, i) => Object .entries (obj) .reduce (
      (a, [k, v]) => ({... a , [k]: v[i] }),
      {}
    )
  )

// Main function
const finalFunction = (
  {array: [headers, ...rows], ...rest}, 
  callback,
  changes = rows.map(r => transposeObj(callback(r).changes)),
  allHeaders = [
    ...headers, 
    ...changes 
      .flatMap (t => t .flatMap (Object.keys) )
      .filter (k => !headers .includes (k))
      .filter ((x, i, a) => a .indexOf (x) == i)
  ],
) => ({
  array: [
    allHeaders,
    ...rows .flatMap (
      (row, i) => changes [i] .map (
        change => Object .entries (change) .reduce (
          (r, [k, v]) => [
            ...r.slice(0, allHeaders .indexOf (k)), 
            v, 
            ...r.slice(allHeaders .indexOf (k) + 1)
          ],
          row.slice(0)
        )
      )
    )
  ], 
  ...rest
})


const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]], more: 'stuff', goes: 'here'}

// Faked out to attmep
const callback1 = (row) => ({changes: {FirstName: [row[1][0].toUpperCase() + row[1].slice(1)]}})
const callback2 = (row) => ({changes: {FullName: [`${row[1]} ${row[2]}`]}})
const callback3 = (row) => ({changes: {Email: [`${row[1]}.${row[2]}@stackoverflow.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}}) 

console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))

При этом используется вспомогательная функция transposeObj, которая преобразует списки changes в то, что я считаю более полезным. Это превращает это:

{
  Email: ["tim.foo@stackoverflow.com", "timmy@gmail.com"],
  MailType: ["Work", "Personal"]
}

в это:

[
  {Email: "tim.foo@stackoverflow.com", MailType: "Work"}, 
  {Email: "timmy@gmail.com",           MailType: "Personal"}
]

Основная функция принимает ваш обратный вызов и объект данных с параметром array, из которого извлекает headers иМассивы rows (а также отслеживание оставшихся свойств в rest.) Он получает changes, вызывая хелпер transposeObj для свойства changes, являющегося результатом вызова обратного вызова для каждой строки. Используя эти данные, он находит новые заголовки, получая все ключи в объектах changes, удаляя все, что уже есть в массиве, а затем сокращая до набора уникальных значений. Затем он добавляет эти новые к существующим заголовкам для получения allHeaders.

. В теле функции мы возвращаем новый объект, используя ...rest для других параметров, и обновляем array, запускаяс этим новым списком заголовков, а затем с плоским отображением rows с функцией, которая берет каждый из этих транспонированных объектов и добавляет все его свойства к копии текущей строки, сопоставляя индексы с allHeaders, чтобы поместить их в

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

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

Мы запускаем каждый из них отдельно для вашего ввода, генерируя три отдельных объекта результата. Обратите внимание, что это не изменяет ваши входные данные. Если вы хотите применить их последовательно, вы можете сделать что-то вроде:

const data1 = finalFunction (data, callback1)
console.log (data1, '-----------------------------------')
const data2 = finalFunction (data1, callback2)
console.log (data2, '-----------------------------------')
const data3 = finalFunction (data2, callback3)
console.log (data3, '-----------------------------------')

, чтобы получить результат, например:

{
    array: [
        ["#", "FirstName", "LastName"],
        ["1", "Tim", "foo"],
        ["2", "Kim", "bar"]
    ],
    more: "stuff",
    goes: "here"
}
-----------------------------------
{
    array: [
        ["#", "FirstName", "LastName", "FullName"],
        ["1", "Tim","foo", "Tim foo"],
        ["2", "Kim", "bar", "Kim bar"]
    ],
    more: "stuff",
    goes: "here"
}
-----------------------------------
{
    array: [
        ["#", "FirstName", "LastName", "FullName", "Email", "MailType"],
        ["1", "Tim", "foo", "Tim foo", "Tim.foo@stackoverflow.com", "Work"],
        ["1", "Tim", "foo", "Tim foo", "Timmy@gmail.com", "Personal"],
        ["2", "Kim", "bar", "Kim bar", "Kim.bar@stackoverflow.com", "Work"],
        ["2", "Kim", "bar", "Kim bar", "Kimmy@gmail.com", "Personal"]
    ],
    more: "stuff",
    goes: "here"
}
-----------------------------------

Или, конечно, вы можете просто начать let data = ... и затем выполните data = finalFunction(data, nextCallback) в некотором цикле.

Эта функция сильно зависит от flatMap, который доступен не во всех средах. Страница MDN предлагает альтернативы, если они вам нужны. Если вы по-прежнему используете Ramda, будет работать функция chain.


Обновление

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

// Helper function
const transposeObj = (obj) =>
  map (
    (i) => reduce((a, [k, v]) => ({ ...a, [k]: v[i] }), {}, toPairs(obj)),
    range (0, length (values (obj) [0]) )
  )

// Main function
const finalFunction = (
  { array: [headers, ...rows], ...rest },
  callback,
  changes = map (pipe (callback, prop('changes'), transposeObj), rows),
  allHeaders = uniq (concat (headers, chain (chain (keys), changes)))
) => ({
  array: concat([allHeaders], chain(
    (row) => map (
      pipe (
        toPairs,
        reduce((r, [k, v]) => assocPath([indexOf(k, allHeaders)], v, r), row)
      ),
      changes[indexOf(row, rows)]
    ),
    rows
  )),
  ...rest
})

const data = {array: [["#", "FirstName", "LastName"], ["1", "tim", "foo"], ["2", "kim", "bar"]], more: 'stuff', goes: 'here'}

// Faked out to attmep
const callback1 = (row) => ({changes: {FirstName: [row[1][0].toUpperCase() + row[1].slice(1)]}})
const callback2 = (row) => ({changes: {FullName: [`${row[1]} ${row[2]}`]}})
const callback3 = (row) => ({changes: {Email: [`${row[1]}.${row[2]}@stackoverflow.com`,`${row[1]}my@gmail.com`],MailType: ["Work","Personal"]}}) 

console .log (finalFunction (data, callback1))
console .log (finalFunction (data, callback2))
console .log (finalFunction (data, callback3))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {map, reduce, toPairs, range, length, values, pipe, prop, uniq, concat, chain, keys, assocPath, indexOf} = R </script>
...