Сортировать объекты в массиве с помощью динамических вложенных ключей свойств - PullRequest
0 голосов
/ 17 февраля 2019

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

Пока у меня есть этот код

sortBy = (isReverse=false) => {
    this.setState(prevState => ({
        files: prevState.files.sort((a, b) => {
            const valueA = (((a || {})['general'] || {})['fileID']) || '';
            const valueB = (((b || {})['general'] || {})['fileID']) || '';

            if(isReverse) return valueB.localeCompare(valueA);

            return valueA.localeCompare(valueB);
        })
    }));
}

На данный момент ключи жестко закодированы['general']['orderID'] но я хочу, чтобы эта часть была динамичной, добавив параметр keys к функции sortBy:

sortBy = (keys, isReverse=false) => { ...

keys - это массив с вложенными ключами.Для приведенного выше примера это будет ['general', 'fileID'].

Какие шаги необходимо предпринять, чтобы сделать эту динамику?

Примечание: дочерние объекты могут быть неопределенными, поэтому яЯ использую a || {}

Примечание 2: я использую es6.Нет внешних пакетов.

Ответы [ 7 ]

0 голосов
/ 17 февраля 2019

Это также обрабатывает случай, когда путь преобразуется в не строковое значение путем преобразования его в строку.В противном случае .localeCompare может завершиться ошибкой.

sortBy = (keys, isReverse=false) => {
    this.setState(prevState => ({
        files: prevState.files.sort((a, b) => {
            const valueA = getValueAtPath(a, keys);
            const valueB = getValueAtPath(b, keys);

            if(isReverse) return valueB.localeCompare(valueA);

            return valueA.localeCompare(valueB);
        })
    }));
}

function getValueAtPath(file, path) {
    let value = file;
    let keys = [...path]; // preserve the original path array

    while(value && keys.length) {
      let key = keys.shift();
      value = value[key];
    }

    return (value || '').toString();
}
0 голосов
/ 17 февраля 2019

В настоящее время принятый ответ, за исключением исправления ошибок в вашем коде , не сильно поможет вам.Использование простой функции deepProp уменьшит болезненное повторение -

const deepProp = (o = {}, props = []) =>
  props.reduce((acc = {}, p) => acc[p], o)

Теперь без особого шума -

sortBy = (keys, isReverse = false) =>
  this.setState ({
    files: // without mutating the previous state!
      [...this.state.files].sort((a,b) => {
        const valueA = deepProp(a, keys) || ''
        const valueB = deepProp(b, keys) || ''
        return isReverse
          ? valueA.localeCompare(valueB)
          : valueB.localeCompare(valueA)
      })
  })

Тем не менее, это мало что дает с точки зрения реального улучшения вашей программы,Он изобилует сложностью, и что еще хуже, эта сложность будет продублирована в любом компоненте, который требует аналогичных функций.React охватывает функциональный стиль, поэтому этот ответ подходит к проблеме с функциональной точки зрения.В этом посте мы напишем sortBy как -

sortBy = (comparator = asc) =>
  this.setState
    ( { files:
          isort
            ( contramap
                ( comparator
                , generalFileId
                )
            , this.state.files
            )
      }
    )

Ваш вопрос ставит нас перед изучением двух мощных функциональных концепций;мы используем их, чтобы ответить на вопрос -

  1. Монады
  2. Контравариантные Функторы

Давайте не будем перегружены терминами и вместо этого сосредоточимся на полученииинтуиция о том, как все работает.Во-первых, похоже, что у нас есть проблема проверки на нулевые значения.Необходимость учитывать, что некоторые из наших входных данных могут не иметь вложенных свойств, делает нашу функцию беспорядочной.Если мы сможем обобщить эту концепцию возможного значения , мы сможем кое-что почистить.

Ваш вопрос, в частности, говорит, что вы сейчас не используете внешний пакет, но это хорошовремя, чтобы достичь одного.Давайте кратко рассмотрим пакет data.maybe -

Структура для значений, которые могут отсутствовать, или для вычислений, которые могут не работать.Maybe(a) явно моделирует эффекты, которые неявно присутствуют в типах Nullable, поэтому не имеет никаких проблем, связанных с использованием null или undefined - например, NullPointerException или TypeError.

Похоже, хорошо подходит.Мы начнем с написания функции safeProp, которая принимает объект и строку свойства в качестве входных данных.Интуитивно safeProp безопасно возвращает свойство p объекта o -

const { Nothing, fromNullable } =
  require ('data.maybe')

const safeProp = (o = {}, p = '') =>

  // if o is an object
  Object (o) === o

    // access property p on object o, wrapping the result in a Maybe
    ? fromNullable (o[p])

    // otherwise o is not an object, return Nothing
    : Nothing ()

Вместо простого возврата o[p], который может бытьнулевое или неопределенное значение, мы вернемся Возможно , которое поможет нам в обработке результата -

const generalFileId = (o = {}) =>

  // access the general property
  safeProp (o, 'general')

    // if it exists, access the fileId property on the child
    .chain (child => safeProp (child, 'fileId'))

    // get the result if valid, otherwise return empty string
    .getOrElse ('') 

Теперь у нас есть функция, которая может принимать объекты различной сложности и гарантирует результат, который нас интересует -

console .log
  ( generalFileId ({ general: { fileId: 'a' } })  // 'a'
  , generalFileId ({ general: { fileId: 'b' } })  // 'b'
  , generalFileId ({ general: 'x' })              // ''
  , generalFileId ({ a: 'x '})                    // ''
  , generalFileId ({ general: { err: 'x' } })     // ''
  , generalFileId ({})                            // ''
  )

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

Я намеренно избегаю показывать здесь реализацию Maybe, поскольку это само по себе является ценным уроком.Когда модуль обещает возможность X , мы предполагаем, что у нас есть возможность X , и игнорируем то, что происходит в черном ящике модуля.Суть абстракции данных заключается в том, чтобы скрыть проблемы, чтобы программист мог думать о вещах на более высоком уровне.

Может возникнуть вопрос, как работает массив?Как он вычисляет или корректирует свойство length, когда элемент добавляется или удаляется из массива?Как функция map или filter создает массив new ?Если вы никогда не задумывались об этом раньше, это нормально!Массив является удобным модулем, потому что он устраняет эти проблемы из головы программиста;это просто работает как рекламируется .

Это применимо независимо от того, предоставляется ли модуль JavaScript, третьей стороной, такой как из npm, или если вы написали модуль самостоятельно.Если Array не существует, мы могли бы реализовать его как нашу собственную структуру данных с эквивалентными удобствами.Пользователи нашего модуля получают полезные функции без , вносящие дополнительную сложность.А-ха наступает момент, когда вы понимаете, что программист является его / ее собственным пользователем: когда вы сталкиваетесь с трудной проблемой, напишите модуль, чтобы избавиться от оков сложности. Придумайте свое удобство!

Мы продемонстрируем базовую реализацию Maybe позже в ответе, но сейчас нам просто нужно закончить сортировку ...


Мы начнем с двух основных компараторов, asc для сортировки по возрастанию и desc для сортировки по убыванию -

const asc = (a, b) =>
  a .localeCompare (b)

const desc = (a, b) =>
  asc (a, b) * -1

В React мы не можем изменить предыдущее состояние, вместо этого мы должны создать новое состояние.Таким образом, для сортировки неизменным мы должны реализовать isort, который не будет изменять объект ввода -

const isort = (compare = asc, xs = []) =>
  xs
    .slice (0)      // clone
    .sort (compare) // then sort

И, конечно, a и b иногда являются сложными объектами, поэтомуВ случае, если мы не можем напрямую позвонить asc или desc.Ниже contramap преобразует наши данные, используя одну функцию g, перед передачей данных в другую функцию, f -

const contramap = (f, g) =>
  (a, b) => f (g (a), g (b))

const files =
  [ { general: { fileId: 'e' } }
  , { general: { fileId: 'b' } }
  , { general: { fileId: 'd' } }
  , { general: { fileId: 'c' } }
  , { general: { fileId: 'a' } }
  ]

isort
  ( contramap (asc, generalFileId) // ascending comparator
  , files
  )

// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]

Используя другой компаратор desc мы можем видеть работу сортировки в другом направлении -

isort
  ( contramap (desc, generalFileId) // descending comparator
  , files 
  )

// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]

Теперь, чтобы написать метод для вашего компонента React, sortBy.Метод существенно сокращен до this.setState({ files: t (this.state.files) }), где t - это неизменное преобразование состояния вашей программы.Это хорошо, потому что сложность сохраняется вне ваших компонентов, где тестирование затруднено, и вместо этого оно находится в универсальных модулях, которые легко тестировать -

sortBy = (reverse = true) =>
  this.setState
    ( { files:
          isort
            ( contramap
                ( reverse ? desc : asc
                , generalFileId
                )
            , this.state.files
            )
      }
    )

При этом используется логический переключателькак в вашем первоначальном вопросе, но поскольку React включает функциональный шаблон, я думаю, что он будет еще лучше в качестве функции более высокого порядка -

sortBy = (comparator = asc) =>
  this.setState
    ( { files:
          isort
            ( contramap
                ( comparator
                , generalFileId
                )
            , this.state.files
            )
      }
    )

Если не гарантируется доступ к вложенному свойству, к которому вам нужно получить доступчтобы быть general и fileId, мы можем сделать универсальную функцию, которая принимает список свойств и может искать вложенное свойство любой глубины -

const deepProp = (o = {}, props = []) =>
  props .reduce
    ( (acc, p) => // for each p, safely lookup p on child
        acc .chain (child => safeProp (child, p))
    , fromNullable (o) // init with Maybe o
    )

const generalFileId = (o = {}) =>
  deepProp (o, [ 'general', 'fileId' ]) // using deepProp
    .getOrElse ('')

const fooBarQux = (o = {}) =>
  deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
    .getOrElse (0)                      // customizable default

console.log
  ( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
  , generalFileId ({})                            // ''
  , fooBarQux ({ foo: { bar: { qux: 1 } } } )     // 1
  , fooBarQux ({ foo: { bar: 2 } })               // 0
  , fooBarQux ({})                                // 0
  )

Выше мы используем пакет data.maybe, который дает нам возможность работать с потенциальными значениями .Модуль экспортирует функции для преобразования обычных значений в Maybe и наоборот, а также множество полезных операций, применимых к потенциальным значениям.Однако ничто не заставляет вас использовать именно эту реализацию.Концепция достаточно проста, чтобы вы могли реализовать fromNullable, Just и Nothing в пару десятков строк, которые мы увидим позже в этом ответе -

Запустите полную демонстрацию ниже на repl.it

const { Just, Nothing, fromNullable } =
  require ('data.maybe')

const safeProp = (o = {}, p = '') =>
  Object (o) === o
    ? fromNullable (o[p])
    : Nothing ()

const generalFileId = (o = {}) =>
  safeProp (o, 'general')
    .chain (child => safeProp (child, 'fileId'))
    .getOrElse ('')

// ----------------------------------------------
const asc = (a, b) =>
  a .localeCompare (b)

const desc = (a, b) =>
  asc (a, b) * -1

const contramap = (f, g) =>
  (a, b) => f (g (a), g (b))

const isort = (compare = asc, xs = []) =>
  xs
    .slice (0)
    .sort (compare)

// ----------------------------------------------
const files =
  [ { general: { fileId: 'e' } }
  , { general: { fileId: 'b' } }
  , { general: { fileId: 'd' } }
  , { general: { fileId: 'c' } }
  , { general: { fileId: 'a' } }
  ]

isort
  ( contramap (asc, generalFileId)
  , files
  )

// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]

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

Наконец, sortBy реализован как более высокий порядокфункция, которая означает, что мы не ограничены только восходящими и нисходящими сортировками, переключаемыми логическим значением reverse;любой действительный компаратор может быть использован.Это означает, что мы могли бы даже написать специализированный компаратор, который обрабатывает разрывы связей, используя собственную логику, или сравнивает сначала year, затем month, затем day и т. Д .;Функции высшего порядка значительно расширяют ваши возможности.


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

// Maybe.js
const None =
  Symbol ()

class Maybe
{ constructor (v)
  { this.value = v }

  chain (f)
  { return this.value == None ? this : f (this.value) }

  getOrElse (v)
  { return this.value === None ? v : this.value }
}

const Nothing = () =>
  new Maybe (None)

const Just = v =>
  new Maybe (v)

const fromNullable = v =>
  v == null
    ? Nothing ()
    : Just (v)

module.exports =
  { Just, Nothing, fromNullable } // note the class is hidden from the user

Тогда мы будем использовать его в нашем модуле.Нам нужно только изменить импорт (require), но все остальное просто работает как есть из-за общедоступного API-интерфейса совпадений нашего модуля -

const { Just, Nothing, fromNullable } =
  require ('./Maybe') // this time, use our own Maybe

const safeProp = (o = {}, p = '') => // nothing changes here
  Object (o) === o
    ? fromNullable (o[p])
    : Nothing ()

const deepProp = (o, props) => // nothing changes here
  props .reduce
    ( (acc, p) =>
        acc .chain (child => safeProp (child, p))
    , fromNullable (o)
    )

// ...

Для получения дополнительной информации о том, как использовать контркартуи, возможно, некоторые неожиданные сюрпризы, пожалуйста, изучите следующие связанные ответы -

  1. мультисортировка с использованием контркарты
  2. рекурсивный поиск с использованием контркарты
0 голосов
/ 17 февраля 2019

Сравнить элементы в функции сортировки следующим образом:

let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));

likte this:

sortBy = (keys, isReverse=false) => {
    this.setState(prevState => ({
        files: prevState.files.sort((a, b) => {
            let v=c=>keys.reduce((o,k) => o[k]||'',c)
            return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
        })
    }));
}

Вот пример того, как работает эта идея:

let files = [
 { general: { fileID: "3"}},
 { general: { fileID: "1"}},
 { general: { fileID: "2"}},
 { general: { }}
];


function sortBy(keys, arr, isReverse=false) {
    arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>             
      (isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )        
}


sortBy(['general', 'fileID'],files,true);
console.log(files);
0 голосов
/ 17 февраля 2019

Вы можете зациклить ключи, чтобы получить значения, а затем сравнить их как

sortBy = (keys, isReverse=false) => {

    this.setState(prevState => ({
        files: prevState.files.sort((a, b) => {
            const clonedKey = [...keys];
            let valueA = a;
            let valueB = b
            while(clonedKey.length > 0) {
                const key = clonedKey.shift();
                valueA = (valueA || {})[key];
                valueB = (valueB || {})[key];
            }
            valueA = valueA || '';
            valueB = valueB || '';
            if(isReverse) return valueB.localeCompare(valueA);

            return valueA.localeCompare(valueB);
        })
    }));
}
0 голосов
/ 17 февраля 2019

Одним из способов может быть использование lower () для нового аргумента keys, что-то вроде этого:

sortBy = (keys, isReverse=false) =>
{
    this.setState(prevState =>
    ({
        files: prevState.files.slice().sort((a, b) =>
        {
            const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
            const valueB = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
            return (isReverse ? valueB.localeCompare(valueA) : valueA.localeCompare(valueB));
        })
    }));
}
0 голосов
/ 17 февраля 2019

Чтобы работать с произвольным числом ключей, вы можете создать функцию, которую можно использовать повторно с .reduce() для глубокого перехода во вложенные объекты.Я бы также поставил ключи в качестве последнего параметра, чтобы вы могли использовать синтаксис «rest» и «spread».

const getKey = (o, k) => (o || {})[k];

const sorter = (isReverse, ...keys) => (a, b) => {
  const valueA = keys.reduce(getKey, a) || '';
  const valueB = keys.reduce(getKey, b) || '';

  if (isReverse) return valueB.localeCompare(valueA);

  return valueA.localeCompare(valueB);
};

const sortBy = (isReverse = false, ...keys) => {
  this.setState(prevState => ({
    files: prevState.files.sort(sorter(isReverse, ...keys))
  }));
}

Я также переместил функцию сортировки в свою собственную переменную const и заставил ее вернуть новую функцию, которая использует значение isReverse.

0 голосов
/ 17 февраля 2019

Вы можете использовать цикл для извлечения пути вложенного свойства из объекта:

const obj = {
  a: {
    b: {
      c: 3
    }
  } 
}

const keys = ['a', 'b', 'c']

let value = obj;
for (const key of keys) {
  if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
  value = value[key];
}

console.log(`c=${value}`);

Затем вы можете обернуть вышеупомянутую функцию в помощник:

function getPath(obj, keys) {
  let value = obj;
  for (const key of keys) {
    if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
    value = value[key];
  }
  return value;
}

И использовать ее при получении ваших значений:

sortBy = (isReverse = false, keys = []) => {
  this.setState(prevState => ({
    files: prevState.files.sort((a, b) => {
      const valueA = getPath(a, keys) || '';
      const valueB = getPath(b, keys) || '';

      // ...
    })
  }));
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...