【问题标题】:javascript object array to json m-level pivot table-style data formatjavascript对象数组到json m级数据透视表样式数据格式
【发布时间】:2021-03-28 22:27:20
【问题描述】:

我要做的是从 javascript 对象数组创建数据透视表样式的数据格式。

我已经尝试对此进行研究,但我觉得我在苦苦挣扎,部分原因是我没有使用正确的语言来描述我正在寻找的东西。

这里有一些背景

我经常使用 excel 中的数据和数据透视表功能。

我编写了一个简单的 VBA 宏来将 excel 表作为对象数组保存到 json 文件中。

所以这里有这样一张表:

然后我将其转换为如下内容。这部分对我来说很容易。

let objArr = [
 {"CAT1":"UP","CAT2":"LIGHT-BLUE","CAT3":"INLINE","VAL":"651"},
 {"CAT1":"UP","CAT2":"LIGHT-BLUE","CAT3":"INLINE","VAL":"683"},
 {"CAT1":"UP","CAT2":"MILD-GREEN","CAT3":"STRANGE","VAL":"189"},
 {"CAT1":"UP","CAT2":"MILD-GREEN","CAT3":"INSIDE","VAL":"113"},
 {"CAT1":"LEFT","CAT2":"HOT-PINK","CAT3":"INSIDE-OUT","VAL":"899"},
 {"CAT1":"LEFT","CAT2":"HOT-PINK","CAT3":"INSIDE-OUT","VAL":"901"},
 {"CAT1":"LEFT","CAT2":"BORON-COLOR","CAT3":"FLAT","VAL":"345"},
 {"CAT1":"LEFT","CAT2":"BORON-COLOR","CAT3":"OUTLINE","VAL":"678"},
 {"CAT1":"LEFT","CAT2":"PALE-BLUE","CAT3":"MOST-SHAPE","VAL":"611"},
 {"CAT1":"LEFT","CAT2":"PALE-BLUE","CAT3":"DARK","VAL":"942"},
 {"CAT1":"UP","CAT2":"LIGHT-RED","CAT3":"ROUND","VAL":"769"}
]

所以我可以在 javascript 中处理对象数组,这一切都非常好用。但我很难把它变成某些项目所需的东西。

这是 excel 中的数据透视表,其中包含第一个表中的数据:

所以无论在 excel 中发生什么过程将表 1 转换为表 2,我都想复制它,但是在 javascript 中,从上面的对象数组变成这样的:

pivotArr = [
 {
  "LEFT":[
   "BORON-COLOR":[
    "FLAT":345,
    "OUTLINE":678
   ],
   "HOT-PINK":[
    "INSIDE-OUT":1800
   ],
   "PALE-BLUE":[
    "DARK":942,
    "MOST-SHAPE":611
   ]
  ],
  "UP":[
   "LIGHT-BLUE":[
    "INLINE":1334
   ],
   "LIGHT-RED":[
    "ROUND":769
   ],
   "MILD-GREEN":[
    "INSIDE":113,
    "STRANGE":189
   ]
  ]
 }
]

我在想象这样的事情:

let pivotArr = convertObjArr2PivotArr(objArr, key0, key1, ....)

在上述情况下,对象数组为 objArrkey0="CAT1"key1="CAT2" 等。

我尝试过的:

现在,如果我只有 1 个类别,那就很容易了。

这不是我想要的,但它几乎就在那里:

function groupObjArr(objArr, key, val) {
  let obj = {};
  for (let i = 0; i < objArr.length; i++) { 
   obj[objArr[i][key]] = (obj[objArr[i][key]] + parseFloat(objArr[i][val]) || parseFloat(objArr[i][val]));
  }
  return obj;
}

困难的部分是按任意数量的级别聚合数据,m

如果我有已知数量的关卡,我可以将其放大。 IE。总是 2 个级别/节点/键等(我认为这就像一棵树)。

但我可能有m 级别,我不知道该怎么做。

我尝试过逐行分组,也尝试对整列(键)进行分组,然后向左移动(到更大的组),还从最大的组开始,然后添加子组。

每次我尝试这个我都会更加困惑。

  • 是否有适用于此类程序的既定算法?
  • 或者它有名字吗?
  • 或者有没有我可以遵循的伪代码算法?

【问题讨论】:

  • 这能回答你的问题吗? Pivot or Transforming JavaScript object
  • 我会说它没有,除非它可以扩展到 m 级。或者,如果确实如此,我的答案并不清楚。那是一个级别(即将数组中的所有数据聚合到公共邮政编码中。)我可能遗漏了有关如何将其扩展到多个级别的信息。如果您检查我的问题中的数据透视表,首先它按CAT1 聚合,然后在CAT1 内按CAT2 等聚合。

标签: javascript arrays json algorithm pivot-table


【解决方案1】:

这是一个使用 Array.prototype.reduce() 的示例,它适应 m 级。

使用destructuring,我们在value 和将用作分支的其余键之间进行初始分离。我们检索这些分支的Object.values(),因为没有使用密钥。这些值中的最后两个将用于构造分支的最内层数组,我们使用第二个 .reduce() 调用,以外部累加器为种子,来构建或检索所需的分支。最后我们在返回的终端对象上构造内部数组,并将叶子对象推入其中。

注意Logical nullish assignment (??=) 的使用,您可能需要将其替换为OR 短路以实现兼容性

const input = [{ "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "651" }, { "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "683" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "STRANGE", "VAL": "189" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "INSIDE", "VAL": "113" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "899" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "901" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "FLAT", "VAL": "345" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "OUTLINE", "VAL": "678" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "MOST-SHAPE", "VAL": "611" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "DARK", "VAL": "942" }, { "CAT1": "UP", "CAT2": "LIGHT-RED", "CAT3": "ROUND", "VAL": "769" }];

const pivot = input.reduce((acc, { VAL, ...cats }) => {
  const branches = Object.values(cats);
  const [leafKey, leaf] = branches.splice(-2);

  const terminal = branches.reduce((_acc, branch) => (
    _acc[branch] ??= {}
  ), acc);

  (terminal[leafKey] ??= []).push({ [leaf]: VAL });
  
  return acc;
}, {});

console.log(pivot)
.as-console-wrapper { max-height: 100% !important; top: 0; }

编辑

上面的 sn-p 适用于给定的输入,但在很多方面都很脆弱。

它的主要弱点是它依赖于对象属性的顺序,并假设此顺序将在对象之间保持一致,即使使用recent standardization of ordering,也不可靠,尤其是对象之间。要解决此问题,最好明确定义分支排序。

除此之外,预期的结构要求至少有 2 个级别才能获得有意义的结果,因此您至少可以检查这一点,如果不满足,请尽早返回。

const input = [{ "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "651" }, { "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "683" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "STRANGE", "VAL": "189" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "INSIDE", "VAL": "113" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "899" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "901" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "FLAT", "VAL": "345" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "OUTLINE", "VAL": "678" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "MOST-SHAPE", "VAL": "611" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "DARK", "VAL": "942" }, { "CAT1": "UP", "CAT2": "LIGHT-RED", "CAT3": "ROUND", "VAL": "769" }];

function pivot(arr, { path, value }) {
  if (path.length < 2) {
    console.log('Pivot called with too few path elements');
    return
  }
  return arr.reduce((acc, { [value]: val, ...rest }) => {
    const branches = path.map(k => rest[k]);
    const [leafKey, leaf] = branches.splice(-2);

    const terminal = branches.reduce((_acc, branch) => (
      _acc[branch] ??= {}
    ), acc);

    (terminal[leafKey] ??= []).push({ [leaf]: val });

    return acc;
  }, {});
}

console.log(pivot(input, { path: ['CAT1', 'CAT2', 'CAT3'], value: 'VAL' }))
.as-console-wrapper { max-height: 100% !important; top: 0; }

为了更好地理解内部 .reduce() 调用,您可以将其重写为带有外部累加器的 .forEach()

let terminal = acc;
branches.forEach(branch => {
  if (!(branch in terminal)) {
    terminal[branch] = {};
  }
  terminal = terminal[branch];
})

硬编码

如果结构不太可能改变,您也可以简单地对结构进行硬编码(也是您关于如何扩展链接副本的问题的答案)。为此,您只需在推送最终值之前断言每个级别都存在。

const input = [{ "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "651" }, { "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "683" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "STRANGE", "VAL": "189" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "INSIDE", "VAL": "113" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "899" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "901" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "FLAT", "VAL": "345" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "OUTLINE", "VAL": "678" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "MOST-SHAPE", "VAL": "611" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "DARK", "VAL": "942" }, { "CAT1": "UP", "CAT2": "LIGHT-RED", "CAT3": "ROUND", "VAL": "769" }];

const pivot = input
  .reduce((acc, { CAT1, CAT2, CAT3, VAL }) => {
  
    acc[CAT1] = acc[CAT1] || {};
    acc[CAT1][CAT2] = acc[CAT1][CAT2] || [];
    
    acc[CAT1][CAT2].push({ [CAT3]: VAL });

    return acc;
  }, {});

console.log(pivot)
.as-console-wrapper { max-height: 100% !important; top: 0; }

【讨论】:

  • 我将不得不研究这个并阅读 reduce 方法以更好地理解它。这怎么能汇总最终级别?例如,“向上”>“浅蓝色”>“内联”?
  • 内部 reduce() 调用在 acc 上查找现有分支或将其创建为空对象,然后将该分支传递给下一次迭代。所以当 reduce 完成时,terminal 是对最内层分支的引用。 “light-blue”其实就是上面sn-p中的leafKey,而“inline”就是leaf。终端分配reduce() 调用可以重写为带有外部累加器的forEach,甚至可以重写为递归函数。
【解决方案2】:

基于两个简单的辅助函数 groupmapObj,这可能会为您解决问题:

const pivot = ([key, ...keys], combine = (x) => x) => (xs) =>
  key == undefined
    ? combine (xs)
    : mapObj (pivot (keys, combine)) (group (key) (xs))

对我们想要收集和嵌套的键列表使用简单的递归,然后我们可以调用pivot (['CAT1', 'CAT2', 'CAT3']) (input) 来获得如下结果:

{
  "UP": {
    "LIGHT-BLUE": {
      "INLINE": [
        {"VAL": "651"},
        {"VAL": "683"}
      ]
    },
    "MILD-GREEN": {
      "STRANGE": [
        {"VAL": "189"}
      ],
      // ...
    },
    // ...
  },
  // ...
}

通过传递一个附加参数来解释我们想要对最里面的值做什么,我们可以调用pivot (['CAT1', 'CAT2', 'CAT3'], sumBy ('VAL')) (input) 来获取您请求的结构

{
  "UP": {
    "LIGHT-BLUE": {
      "INLINE": 1334
    },
    "MILD-GREEN": {
      "STRANGE": 189,
      // ...
    },
    // ...
  },
  // ...
}

所有这些功能都在这个sn-p中:

const group = (key) => (xs) => 
  xs .reduce ((a, {[key]: k, ...rest}) => ({...a, [k]: [...(a[k] || []), rest]}), {})

const sumBy = (key) => (xs) =>
  xs .reduce ((a, x) => a + Number (x [key]), 0)

const mapObj = (fn) => (o) =>
  Object .fromEntries (Object .entries (o) .map (([k, v]) => [k, fn(v)]))

const pivot = ([key, ...keys], combine = (x) => x) => (xs) =>
  key == undefined
    ? combine (xs)
    : mapObj (pivot (keys, combine)) (group (key) (xs))

const input = [{ "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "651" }, { "CAT1": "UP", "CAT2": "LIGHT-BLUE", "CAT3": "INLINE", "VAL": "683" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "STRANGE", "VAL": "189" }, { "CAT1": "UP", "CAT2": "MILD-GREEN", "CAT3": "INSIDE", "VAL": "113" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "899" }, { "CAT1": "LEFT", "CAT2": "HOT-PINK", "CAT3": "INSIDE-OUT", "VAL": "901" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "FLAT", "VAL": "345" }, { "CAT1": "LEFT", "CAT2": "BORON-COLOR", "CAT3": "OUTLINE", "VAL": "678" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "MOST-SHAPE", "VAL": "611" }, { "CAT1": "LEFT", "CAT2": "PALE-BLUE", "CAT3": "DARK", "VAL": "942" }, { "CAT1": "UP", "CAT2": "LIGHT-RED", "CAT3": "ROUND", "VAL": "769" }];

console .log (pivot (['CAT1', 'CAT2', 'CAT3']) (input))
console .log (pivot (['CAT1', 'CAT2', 'CAT3'], sumBy ('VAL')) (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

group 采用属性名称和对象列表,并按该属性的值对它们进行分组,从其内部删除该属性。例如,

group ('foo') ([
  {foo: 'a', bar: 'x', baz: 'p'}, 
  {foo: 'b', bar: 'y', baz: 'p'}, 
  {foo: 'a', bar: 'w', baz: 'm'}
])

会回来

{
  a: [{bar: 'x', baz: 'p'}, {bar: 'w', baz: 'm'}], 
  b: [{bar: 'y', baz: 'p'}]
}

mapObj 类似于数组映射,将函数应用于对象中每个键的值。例如,

map (n => n * n) ({a: 1, b: 2, c: 3, d: 4, e: 5})

产量

{a: 1, b: 4, c: 9, d: 16, e: 25}

sumBy 只是总结匹配给定键的属性,因此

sumBy ('count') ([{id: 'a', count: 3}, {id: 'b', count: 11}, {id: 'c', count: 8}])

产量

22

主函数pivot 只是对第一个键进行分组,然后使用mapObject 递归调用pivot,并在结果对象中的每个值上使用剩余的键。当没有剩余键时,我们在最里面的结构上调用combine 函数。 combine 默认为 identity 函数。

两个小音符:

  • 实际上,我有一个处理数组和对象的map 版本;我可能会在这个函数中使用它而不是mapObj

  • groupmuch less effienct 比可能需要。如果这被证明是应用程序中的瓶颈,我会考虑用改变 reduce 累加器的东西来替换它,而不是在每一步都创建一个新的。但我会坚持这种简单性,除非并且直到我能证明这是一个实际的性能拖累。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-01
    • 1970-01-01
    • 2018-12-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多