【问题标题】:creating top 5 aggregation with ramdajs使用 ramdajs 创建前 5 个聚合
【发布时间】:2019-09-26 00:07:08
【问题描述】:

我想转换这个输入

[
        { country: 'France', value: 100 },
        { country: 'France', value: 100 },
        { country: 'Romania', value: 500 },
        { country: 'England', value: 400 },
        { country: 'England', value: 400 },
        { country: 'Spain', value: 130 },
        { country: 'Albania', value: 4 },
        { country: 'Hungary', value: 3 }
]

进入输出

[
      { country: 'England', value: 800 },
      { country: 'Romania', value: 500 },
      { country: 'France', value: 200 },
      { country: 'Spain', value: 130 },
      { country: 'Other', value: 8 }
]

这基本上是在为前 4 个 + 其他国家/地区进行 价值总和

我正在使用带有ramdajs 的javascript,我只设法在 somehow cumbersome way so far 中做到这一点。

我正在寻找一个优雅的解决方案:有任何函数式程序员能够提供他们的解决方案吗?或者任何有用的 ramda 方法的想法?

【问题讨论】:

  • 请添加您的代码,即使这是一种非常繁琐的方式
  • 答案是否只需要使用该库的功能?
  • 我刚刚编辑了问题以添加指向我的解决方案的可运行版本的链接。关于库,我正在寻找解决这个问题的最实用的方法,我不能仅仅为这个算法引入任何其他的函数库,比如 Iodash,但是原生 javascript 是可以的。
  • 我喜欢这个问题产生的各种解决方案!
  • 请不要为此使用选票。以任何方式选择最适合您的一种。我的是最简洁的之一。 appleapple 的管道更长,但步骤最简单。 customcommander 有迄今为止最详细的说明。选择最适合您的答案。

标签: javascript functional-programming ramda.js


【解决方案1】:

(每一步都得到上一步的输出。最后一切都会放在一起。)

第 1 步:获取总和图

你可以改变这个:

[
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
]

进入这个:

{
  Albania: 4,
  England: 800,
  France: 200,
  Hungary: 3,
  Romania: 500,
  Spain: 130
}

有了这个:

const reducer = reduceBy((sum, {value}) => sum + value, 0);
const reduceCountries = reducer(prop('country'));

第 2 步:将其转换回排序数组

[
  { country: "Hungary", value: 3 },
  { country: "Albania", value: 4 },
  { country: "Spain", value: 130 },
  { country: "France", value: 200 },
  { country: "Romania", value: 500 },
  { country: "England", value: 800 }
]

你可以这样做:

const countryFromPair = ([country, value]) => ({country, value});
pipe(toPairs, map(countryFromPair), sortBy(prop('value')));

第 3 步:创建两个子组,非前 4 名国家和前 4 名国家/地区

[
  [
    { country: "Hungary", value: 3},
    { country: "Albania", value: 4}
  ],
  [
    { country: "Spain", value: 130 },
    { country: "France", value: 200 },
    { country: "Romania", value: 500 },
    { country: "England", value: 800 }
  ]
]

你可以用这个做什么:

splitAt(-4)

第 4 步:合并第一个子组

[
  [
    { country: "Others", value: 7 }
  ],
  [
    { country: "Spain", value: 130 },
    { country: "France", value: 200 },
    { country: "Romania", value: 500 },
    { country: "England", value: 800 }
  ]
]

有了这个:

over(lensIndex(0), compose(map(countryFromPair), toPairs, reduceOthers));

第 5 步:展平整个数组

[
  { country: "Others", value: 7 },
  { country: "Spain", value: 130 },
  { country: "France", value: 200 },
  { country: "Romania", value: 500 },
  { country: "England", value: 800 }
]

flatten

完整的工作示例

const data = [
  { country: 'France', value: 100 },
  { country: 'France', value: 100 },
  { country: 'Romania', value: 500 },
  { country: 'England', value: 400 },
  { country: 'England', value: 400 },
  { country: 'Spain', value: 130 },
  { country: 'Albania', value: 4 },
  { country: 'Hungary', value: 3 }
];

const reducer = reduceBy((sum, {value}) => sum + value, 0);
const reduceOthers = reducer(always('Others'));
const reduceCountries = reducer(prop('country'));
const countryFromPair = ([country, value]) => ({country, value});

const top5 = pipe(
  reduceCountries,
  toPairs,
  map(countryFromPair),
  sortBy(prop('value')),
  splitAt(-4),
  over(lensIndex(0), compose(map(countryFromPair), toPairs, reduceOthers)),
  flatten
);

top5(data)

【讨论】:

  • reduceBy: 我知道我错过了什么!非常好的技术,一个很好的答案。
  • 感谢@ScottSauyet。使用zipObj 从一对中重建对象来查看您的答案感觉比我正在做的更自然。虽然我还没有完全理解lift,但在这种情况下它也感觉更惯用。这个我也会考虑的。
  • 这个discussion of lift 可能会有所帮助。它将对值进行操作的函数转换为对这些值的 容器 进行操作的函数。虽然这可能类似于Maybe,但它也可以是返回该值的函数。这就是我在这里使用的。对于函数,lift(f)(g, h) 类似于(...args) => f(g(...args), h(...args))。这很像converge,但行为更标准(但灵活性稍差)。
【解决方案2】:

这是一种方法:

const combineAllBut = (n) => pipe(drop(n), pluck(1), sum, of, prepend('Others'), of)

const transform = pipe(
  groupBy(prop('country')),
  map(pluck('value')),
  map(sum),
  toPairs,
  sort(descend(nth(1))),
  lift(concat)(take(4), combineAllBut(4)),
  map(zipObj(['country', 'value']))
)

const countries = [{ country: 'France', value: 100 }, { country: 'France', value: 100 }, { country: 'Romania', value: 500 }, { country: 'England', value: 400 }, { country: 'England', value: 400 }, { country: 'Spain', value: 130 }, { country: 'Albania', value: 4 }, { country: 'Hungary', value: 3 }]

console.log(transform(countries))
<script src="https://bundle.run/ramda@0.26.1"></script>
<script>
const {pipe, groupBy, prop, map, pluck, sum, of, prepend, toPairs, sort, descend, nth, lift, concat, take, drop, zipObj} = ramda
</script>

除了一个复杂的行 (lift(concat)(take(4), combineAllBut(4))) 和相关的辅助函数 (combineAllBut),这是一组简单的转换。该辅助函数可能在此函数之外没有用,因此将其内联为 lift(concat)(take(4), pipe(drop(4), pluck(1), sum, of, prepend('Others'), of)) 是完全可以接受的,但我发现生成的函数有点难以阅读。

请注意,该函数将返回类似[['Other', 7]] 的内容,这是一种毫无意义的格式,除了我们将使用前四个数组的concat 之外。因此,至少有一些论点可以删除最终的of 并将concat 替换为flip(append)。我没有这样做,因为这个辅助函数除了在这个管道的上下文中没有任何意义。但如果有人会选择其他方式,我会理解。

我喜欢这个函数的其余部分,它似乎很适合 Ramda 管道风格。但是这个辅助函数在某种程度上破坏了它。我很想听听有关简化它的建议。

更新

然后来自 customcommander 的回答展示了我可以采取的简化方法,即在上述方法中使用 reduceBy 而不是 groupBy -&gt; map(pluck) -&gt; map(sum) 舞蹈。这会带来明显的改善。

const combineAllBut = (n) => pipe(drop(n), pluck(1), sum, of, prepend('Others'), of)

const transform = pipe(
  reduceBy((a, {value}) => a + value, 0, prop('country')),
  toPairs,
  sort(descend(nth(1))),
  lift(concat)(take(4), combineAllBut(4)),
  map(zipObj(['country', 'value']))
)

const countries = [{ country: 'France', value: 100 }, { country: 'France', value: 100 }, { country: 'Romania', value: 500 }, { country: 'England', value: 400 }, { country: 'England', value: 400 }, { country: 'Spain', value: 130 }, { country: 'Albania', value: 4 }, { country: 'Hungary', value: 3 }]

console.log(transform(countries))
<script src="https://bundle.run/ramda@0.26.1"></script>
<script>
const {pipe, reduceBy, prop, map, pluck, sum, of, prepend, toPairs, sort, descend, nth, lift, concat, take, drop, zipObj} = ramda
</script>

【讨论】:

    【解决方案3】:

    我试一试,并尝试将它的功能用于大多数事情。并保持单身pipe

    const f = pipe(
      groupBy(prop('country')),
      map(map(prop('value'))),
      map(sum),
      toPairs(),
      sortBy(prop(1)),
      reverse(),
      addIndex(map)((val, idx) => idx<4?val:['Others',val[1]]),
      groupBy(prop(0)),
      map(map(prop(1))),
      map(sum),
      toPairs(),
      map(([a,b])=>({'country':a,'value':b}))
    )
    

    Ramda REPL


    但是,我认为它没有任何可读性。

    【讨论】:

      【解决方案4】:

      我认为您可以通过在减少数组之前拆分数组来稍微简化groupOthersKeeping,就 ramda 而言,可能如下所示:

      const groupOthersKeeping = contriesToKeep => arr => [
          ...slice(0, contriesToKeep, arr),
          reduce(
            (acc, i) => ({ ...acc, value: acc.value + i.value }),
            { country: 'Others', value: 0 },
            slice(contriesToKeep, Infinity, arr)
          )
       ]
      

      【讨论】:

        【解决方案5】:

        使用更多的 ramda 函数但不确定是否更好:

        let country = pipe(
          groupBy(prop('country')),
          map(pluck('value')),
          map(sum)
        )([
          { country: 'France', value: 100 },
          { country: 'France', value: 100 },
          { country: 'Romania', value: 500 },
          { country: 'England', value: 400 },
          { country: 'England', value: 400 },
          { country: 'Spain', value: 130 },
          { country: 'Albania', value: 4 },
          { country: 'Hungary', value: 3 }
        ]);
        
        let splitCountry = pipe(
          map((k) => ({country: k, value: country[k]})),
          sortBy(prop('value')),
          reverse,
          splitAt(4)
        )(keys(country));
        
        splitCountry[0].push({country: 'Others', value: sum(map(prop('value'))(splitCountry[1]))});
        splitCountry[0]
        

        【讨论】:

          【解决方案6】:

          这是我的两分钱。

          const a = [
              { country: 'France', value: 100 },
              { country: 'France', value: 100 },
              { country: 'Romania', value: 500 },
              { country: 'England', value: 400 },
              { country: 'England', value: 400 },
              { country: 'Spain', value: 130 },
              { country: 'Albania', value: 4 },
              { country: 'Hungary', value: 3 }
          ];
          
          const diff = (a, b) => b.value - a.value;
          const addValues = (acc, {value}) => R.add(acc,value);
          const count = R.reduce(addValues, 0);
          const toCountry = ({country}) => country;
          const toCountryObj = (x) => ({'country': x[0], 'value': x[1] });
          const reduceC = R.reduceBy(addValues, [], toCountry);
          
          const [countries, others] = R.compose(
              R.splitAt(4), 
              R.sort(diff), 
              R.chain(toCountryObj), 
              R.toPairs, 
              reduceC)(a);
          
          const othersArray = [{ 'country': 'Others', 'value': count(others) }];
          
          R.concat(countries, othersArray);
          

          Ramda REPL

          【讨论】:

          • 每个辅助函数都包含一个特殊化,这使得它们难以重用并且最终在函数式程序中感觉不自然
          【解决方案7】:

          我会按国家/地区分组,将每个国家/地区组合并为一个对象,同时对值求和,排序,拆分为两个数组 [highest 4] 和 [others],将其他数组合并为一个对象,并与最高 4.

          const { pipe, groupBy, prop, values, map, converge, merge, head, pluck, sum, objOf, sort, descend, splitAt, concat, last, of, assoc } = R
          
          const sumProp = key => pipe(pluck(key), sum, objOf(key))
          
          const combineProp = key => converge(merge, [head, sumProp(key)])
          
          const getTop5 = pipe(
            groupBy(prop('country')),
            values, // convert to array of country arrays
            map(combineProp('value')), // merge each sub array to a single object
            sort(descend(prop('value'))), // sort descebdubg by the value property
            splitAt(4), // split to two arrays [4 highest][the rest]
            converge(concat, [ // combine the highest and the others object
              head,
              // combine the rest to the others object wrapped in an array
              pipe(last, combineProp('value'), assoc('country', 'others'), of)
            ])
          )
          
          const countries = [{ country: 'France', value: 100 }, { country: 'France', value: 100 }, { country: 'Romania', value: 500 }, { country: 'England', value: 400 }, { country: 'England', value: 400 }, { country: 'Spain', value: 130 }, { country: 'Albania', value: 4 }, { country: 'Hungary', value: 3 }]
          
          const result = getTop5(countries)
          
          console.log(result)
          &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"&gt;&lt;/script&gt;

          【讨论】:

            【解决方案8】:

            我可能会这样做:

            const aggregate = R.pipe(
              R.groupBy(R.prop('country')),
              R.toPairs,
              R.map(
                R.applySpec({ 
                  country: R.head, 
                  value: R.pipe(R.last, R.pluck('value'), R.sum),
                }),
              ),
              R.sort(R.descend(R.prop('value'))),
              R.splitAt(4),
              R.over(
                R.lensIndex(1), 
                R.applySpec({ 
                  country: R.always('Others'), 
                  value: R.pipe(R.pluck('value'), R.sum),
                }),
              ),
              R.unnest,
            );
            
            const data = [
              { country: 'France', value: 100 },
              { country: 'France', value: 100 },
              { country: 'Romania', value: 500 },
              { country: 'England', value: 400 },
              { country: 'England', value: 400 },
              { country: 'Spain', value: 130 },
              { country: 'Albania', value: 4 },
              { country: 'Hungary', value: 3 }
            ];
            
            console.log('result', aggregate(data));
            &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"&gt;&lt;/script&gt;

            【讨论】:

              【解决方案9】:

              这里有两个解决方案

              我认为第二个更容易理解,即使它更长

              “mergeAllWithKeyBy”函数结合了“R.mergeAll”、“R.mergeWithKey”和“R.groupBy”的功能。

              const mergeAllWithKeyBy = R.curry((mergeFn, keyFn, objs) =>
                R.values(R.reduceBy(R.mergeWithKey(mergeFn), {}, keyFn, objs)))
              
              const addValue = (k, l, r) => 
                k === 'value' ? l + r : r
              
              const getTop = 
                R.pipe(
                  mergeAllWithKeyBy(addValue, R.prop('country')),
                  R.sort(R.descend(R.prop('value'))),
                  R.splitAt(4),
                  R.adjust(-1, R.map(R.assoc('country', 'Others'))),
                  R.unnest,
                  mergeAllWithKeyBy(addValue, R.prop('country')),
                )
                
              const data = [
                { country: 'France', value: 100 },
                { country: 'France', value: 100 },
                { country: 'Romania', value: 500 },
                { country: 'England', value: 400 },
                { country: 'England', value: 400 },
                { country: 'Spain', value: 130 },
                { country: 'Albania', value: 4 },
                { country: 'Hungary', value: 3 }
              ]
              
              console.log(getTop(data))
              &lt;script src="//cdn.jsdelivr.net/npm/ramda@latest/dist/ramda.min.js"&gt;&lt;/script&gt;

              const getTop = (data) => {
                const getCountryValue =
                  R.prop(R.__, R.reduceBy((y, x) => y + x.value, 0, R.prop('country'), data))
                  
                const countries = 
                  R.uniq(R.pluck('country', data))
                
                const [topCounties, bottomCountries] = 
                  R.splitAt(4, R.sort(R.descend(getCountryValue), countries))
                
                const others = {
                  country: 'Others', 
                  value: R.sum(R.map(getCountryValue, bottomCountries))
                }
                
                const top =
                  R.map(R.applySpec({country: R.identity, value: getCountryValue}), topCounties)
                
                return R.append(others, top)
              }
              
              const data = [
                { country: 'France', value: 100 },
                { country: 'France', value: 100 },
                { country: 'Romania', value: 500 },
                { country: 'England', value: 400 },
                { country: 'England', value: 400 },
                { country: 'Spain', value: 130 },
                { country: 'Albania', value: 4 },
                { country: 'Hungary', value: 3 }
              ]
              
              console.log(getTop(data))
              &lt;script src="//cdn.jsdelivr.net/npm/ramda@latest/dist/ramda.min.js"&gt;&lt;/script&gt;

              【讨论】:

                猜你喜欢
                • 2017-10-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-03-08
                • 2021-09-14
                • 1970-01-01
                相关资源
                最近更新 更多