【问题标题】:Excel formula section to build an array from table data dynamicallyExcel 公式部分从表数据动态构建数组
【发布时间】:2021-10-24 22:32:06
【问题描述】:

我不知道这是否可能使用单个单元格公式。需要与 Excel 2010 兼容(及更高版本)的公式。 目的是使用公式如

{=SUM(INDEX(built_Array;N(IF(1;ROW(INDIRECT(x1 &":"& x2)))))} 

其中 x1 和 x2 是包含与构建数组兼容的开始索引和结束索引的单元格的引用。

Excel 公式的“内置数组”部分应由其他地方的两个表中的数据构建:用于选择实际内容的表上的标识符(使用查找 excel 函数)。一张表包含重复值的数量,另一张表包含实际值。

例如:

P1 P2 P3 P1 P2 P3
i01 2 4 i01 20.0 20.6
i02 3 i02 10.0
i03 2 7 9 i03 30.0 30.4 30.2
i04 4 2 i04 15.0 15.1
i05 5 i05 10.0

因此为 i03 构建的数组将是

{30.0;30.0;30.4;30.4;30.4;30.4;30.4;30.4;30.4;30.2;30.2;30.2;30.2;30.2;30.2;30.2;30.2;30.2} 

对于 i04 将是

{15.0;15.0;15.0;15.0;15.1;15.1}

那么,上面的数组 (ctrl-shift-enter) 公式中 i04 取第 3 到第 5 个值的结果将为 45.1,而 i04 取第 2 到第 3 个值的结果将为 30。

我发现的困难是从公式中的表构建数组,特别是因为每个标识符可能有不同数量的组件。

我有预感这应该是可行的,在其他编程语言中会使用迭代或递归,但我想探索这种方式而不是恢复到 VBA(如果有概念上的原因这在 Excel 中是不可能的公式我也会很感兴趣,以防我应该改变方法,因为我对公式结果感兴趣)。

【问题讨论】:

  • 所以你基本上需要动态地对这些值求和?因此 i04 的输入应该返回 90.2?
  • @EvilBlueMonkey no - 他需要返回值y 重复x 的次数,其中y 来自右表,x 来自左表
  • 是的,但在那之后你不是将built_Array 的一部分从x1 加到x2 吗?所以输入 i04 1 到 6 输出应该是 90.2,对吧? i04 2 到 3 会返回 30,对吧?
  • 我想出了一个可能的解决方案(直接计算分隔的总和,而不是创建数组),但我需要几天时间才能正确解释。

标签: excel excel-formula dynamic-arrays


【解决方案1】:

为了实现我们的目标,我们需要创建一系列数组,方便地“混合”,将产生我们必须求和的一系列值。此处以图形方式描述了整个过程:

左侧是源数据和用户输入。源数据(起始数组)假定为 2 个范围,每个范围由连续的单元格(区域)组成。这两个范围的一部分,所有其他数据将通过名称表示/计算。这些是需要的名称:

Names ReferenceToR1C1
Target =Sheet1!R4C12 <<<this should return the value to be searched (example: "i03")
MLR_Mtrx_Addr =Sheet1!R5C12 <<<this should return a string reporting the address of the multipliers array (example: "B5:D9")
MLD_Mtrx_Addr =Sheet1!R6C12 <<<this should return a string reporting the address of the multiplied array (example: "G5:I9")
From =Sheet1!R7C12 <<<this should return the starting value from which the formula will sum on (example: 2)
To =Sheet1!R8C12 <<<this should return the ending value to which the formula will sum on (example: 11)
Array01 =(((ROW(INDIRECT("1:"&MLR_Max_Val)))^1)+(COLUMN(INDIRECT("C1:C"&MLR_Col_Cnt,FALSE))^1-1)*MLR_Max_Val)
Array02 =((ROW(INDIRECT("1:"&MLR_Max_Val)))^1*(COLUMN(INDIRECT("C1:C"&MLR_Col_Cnt,FALSE))^0))
Array03 =Array01/((Array02<=INDIRECT(MLR_Tar_Rel_Row_Addr)))
Array04 =AGGREGATE(15,6,Array03,From)
Array05 =AGGREGATE(15,6,Array03,To)
Array06 =IFERROR((Array03>=Array04)*(Array03<=Array05)*INDIRECT(MLD_Tar_Rel_Row_Addr),0)
MLD_Tar_Rel_Row =MATCH(Target,OFFSET(INDIRECT(MLD_Mtrx_Addr),0,-1,,1),0)
MLD_Tar_Rel_Row_Addr =SUBSTITUTE(CELL("address",OFFSET(INDIRECT(MLD_Mtrx_Addr),MLD_Tar_Rel_Row-1,0,1,1))&":"&CELL("address",OFFSET(INDIRECT(MLD_Mtrx_Addr),MLD_Tar_Rel_Row-1,COLUMNS(INDIRECT(MLD_Mtrx_Addr))-1,1,1)),"$","")
MLR_Col_Cnt =COLUMNS(INDIRECT(MLR_Mtrx_Addr))
MLR_Fin_Val_Cnt =SUM(INDIRECT(MLR_Tar_Rel_Row_Addr))
MLR_Max_Val =MAX(INDIRECT(MLR_Tar_Rel_Row_Addr))
MLR_Tar_Rel_Row =MATCH(Target,OFFSET(INDIRECT(MLR_Mtrx_Addr),0,-1,,1),0)
MLR_Tar_Rel_Row_Addr =SUBSTITUTE(CELL("address",OFFSET(INDIRECT(MLR_Mtrx_Addr),MLR_Tar_Rel_Row-1,0,1,1))&":"&CELL("address",OFFSET(INDIRECT(MLR_Mtrx_Addr),MLR_Tar_Rel_Row-1,COLUMNS(INDIRECT(MLR_Mtrx_Addr))-1,1,1)),"$","")
Note
The first five names are still ranged-related. These are the user's input data. The user should modify their ReferenceToR1C1 accordingly to his/her needs as specified by the side notes.

这段代码应该允许我们快速创建所有上述名称:

Sub SubAddNames()
    
    With ActiveWorkbook.Names
        .Add name:="Target", RefersToR1C1:="=Sheet1!R4C12"
        .Add name:="MLR_Mtrx_Addr", RefersToR1C1:="=Sheet1!R5C12"
        .Add name:="MLD_Mtrx_Addr", RefersToR1C1:="=Sheet1!R6C12"
        .Add name:="From", RefersToR1C1:="=Sheet1!R7C12"
        .Add name:="To", RefersToR1C1:="=Sheet1!R8C12"
        .Add name:="Array01", RefersToR1C1:="=(((ROW(INDIRECT(""1:""&MLR_Max_Val)))^1)+(COLUMN(INDIRECT(""C1:C""&MLR_Col_Cnt,FALSE))^1-1)*MLR_Max_Val)"
        .Add name:="Array02", RefersToR1C1:="=((ROW(INDIRECT(""1:""&MLR_Max_Val)))^1*(COLUMN(INDIRECT(""C1:C""&MLR_Col_Cnt,FALSE))^0))"
        .Add name:="Array03", RefersToR1C1:="=Array01/((Array02<=INDIRECT(MLR_Tar_Rel_Row_Addr)))"
        .Add name:="Array04", RefersToR1C1:="=AGGREGATE(15,6,Array03,From)"
        .Add name:="Array05", RefersToR1C1:="=AGGREGATE(15,6,Array03,To)"
        .Add name:="Array06", RefersToR1C1:="=IFERROR((Array03>=Array04)*(Array03<=Array05)*INDIRECT(MLD_Tar_Rel_Row_Addr),0)"
        .Add name:="MLD_Tar_Rel_Row", RefersToR1C1:="=MATCH(Target,OFFSET(INDIRECT(MLD_Mtrx_Addr),0,-1,,1),0)"
        .Add name:="MLD_Tar_Rel_Row_Addr", RefersToR1C1:="=SUBSTITUTE(CELL(""address"",OFFSET(INDIRECT(MLD_Mtrx_Addr),MLD_Tar_Rel_Row-1,0,1,1))&"":""&CELL(""address"",OFFSET(INDIRECT(MLD_Mtrx_Addr),MLD_Tar_Rel_Row-1,COLUMNS(INDIRECT(MLD_Mtrx_Addr))-1,1,1)),""$"","""")"
        .Add name:="MLR_Col_Cnt", RefersToR1C1:="=COLUMNS(INDIRECT(MLR_Mtrx_Addr))"
        .Add name:="MLR_Fin_Val_Cnt", RefersToR1C1:="=SUM(INDIRECT(MLR_Tar_Rel_Row_Addr))"
        .Add name:="MLR_Max_Val", RefersToR1C1:="=MAX(INDIRECT(MLR_Tar_Rel_Row_Addr))"
        .Add name:="MLR_Tar_Rel_Row", RefersToR1C1:="=MATCH(Target,OFFSET(INDIRECT(MLR_Mtrx_Addr),0,-1,,1),0)"
        .Add name:="MLR_Tar_Rel_Row_Addr", RefersToR1C1:="=SUBSTITUTE(CELL(""address"",OFFSET(INDIRECT(MLR_Mtrx_Addr),MLR_Tar_Rel_Row-1,0,1,1))&"":""&CELL(""address"",OFFSET(INDIRECT(MLR_Mtrx_Addr),MLR_Tar_Rel_Row-1,COLUMNS(INDIRECT(MLR_Mtrx_Addr))-1,1,1)),""$"","""")"
    End With
    
End Sub

正确设置名称后,最终公式将是这个:

=IF(OR(INT(From)<>From,INT(To<>To),To>MLR_Fin_Val_Cnt,From>To),#VALUE!,SUM(IFERROR(Array06,0)))

它只能用作数组公式,因此我们需要复制,选择所需的单元格,粘贴公式,然后按 Ctrl+Shift+Enter。

【讨论】:

  • 谢谢邪恶蓝猴。我非常感谢你的努力。我自己有一个可行的解决方案,我打算很快发布。您的解决方案看起来非常好,但我相信我已经设法使用布尔值动态构建数组。 =((ROW(INDIRECT("1:"& e1))>=lb1)*(ROW(INDIRECT("1:"& e1))
【解决方案2】:

对问题的进一步阅读提示了一个避免易失函数的版本。如果需要非易失性方法 1 的工作公式,可以通过将易失性 INDIRECT 替换为非易失性 INDEX 参考形式函数来实现。

一般表达

((ROW(INDIRECT("1:"& n1))>=lb1)*(ROW(INDIRECT("1:"& n1))<=ub1))

转换为

((ROW($A$1:INDEX($A:$A;n1;0))>=lb1)*(ROW($A$1:INDEX($A:$A;n1;0))<=ub1))

其中n1 是 1 列数组中的元素数,lb1 是需要 1 的下限,ub1 是上限。

OFFSET 是唯一的另一个 volatile 函数,只使用一次(获取左侧单元格的下限)。可以用INDEX数组形式函数代替:

IF(COLUMN(set_of_pos)-COLUMN(INDEX(set_of_pos;0;1))=0;0;OFFSET(set_of_pos;0;-1))+1

转换为

IF(COLUMN(set_of_pos)-COLUMN(INDEX(set_of_pos;0;1))=0;0;INDEX(set_of_pos;0;COLUMN(set_of_pos)-COLUMN(INDEX(set_of_pos;0;1)))+1

[等效地,从通用网格中创建一个数组(-1 是偏移代码),

IF(COLUMN($A$1:INDEX($1:$1;0;COLUMNS(set_of_pos)))=1;0;INDEX(set_of_pos;0;COLUMN($A$1:INDEX($1:$1;0;COLUMNS(set_of_pos)))-1))+1

]

其中set_of_pos 是水平范围,它告诉原始帖子的每个部分中的最后一个位置(因此,公式部分在它是第一个 -leftmost- 单元格时给出 0+1 和 1 + 单元格的值到左,从而获得该部分的下界)。

选择一个水平范围的部分单元格后,需要使用Ctrl-Shift-Enter输入公式

{=TRANSPOSE(MMULT(TRANSPOSE(((ROW($A$1:INDEX($A:$A;e1;0))>=IF(COLUMN(set_of_pos)-COLUMN(INDEX(set_of_pos;0;1))=0;0;INDEX(set_of_pos;0;COLUMN(set_of_pos)-COLUMN(INDEX(set_of_pos;0;1)))+1)*(ROW($A$1:INDEX($A:$A;e1;0))<=set_of_pos))*((ROW($A$1:INDEX($A:$A;e1;0))>=x2+1)*(ROW($A$1:INDEX($A:$A;e1;0))<=x1)));--(ROW($A$1:INDEX($A:$A;e1;0))>0)))}

其中e1 是1 列数组中元素的数量(或对包含该元素的单元格的引用),x1x2 是对包含开始索引和结束索引的单元格的引用; set_of_pos 是 RANGE,它告诉每个部分中的最后一个位置,如具有 volatile 函数的答案中定义的那样。 (e1可以通过表达式INDEX(set_of_pos;0;COLUMNS(set_of_pos))得到,给定源数据。)

[可能值得注意的是,由于公式中的所有 INDEX 函数都使用一维范围引用,因此可以省略 0 值参数,因为 INDEX 正确处理了一维的单个附加参数。]

Is there a way to concatenate two arrays in Excel without VBA? [搜索“Without the volatile”,Scott Craner 的评论] 找到 ROW(INDIRECT()) 的非易失性方法。

【讨论】:

    【解决方案3】:

    经过一些研究,找到了该问题的完整有效答案(对 stackoverflow 和贡献者的建议最有帮助,谢谢)。

    方法 1(首选)

    下面的单个单元格 CSE(数组)公式使用以下几种变体

    =((ROW(INDIRECT("1:"& e1))>=lb1)*(ROW(INDIRECT("1:"& e1))<=ub1))
    

    其中e1 是 1 列数组中的元素数,lb1 是需要 1 的下限,ub1 是上限,超出范围元素的值为 0 .

    OP 数据需要简单地转换为建议公式的累积指数

            P1  P2  P3          P1      P2      P3
    i01     2   6   6   i01     20.0    20.6    
    i02     3   3   3   i02     10.0        
    i03     2   9   18  i03     30.0    30.4    30.2
    i04     4   6   6   i04     15.0    15.1    
    i05     5   5   5   i05     10.0        
    

    以下是一个 CSE 公式,将应用于与现有列具有相同列数的范围(上例:3 个连续列),然后在公式栏中插入公式后,使用 ctrl-shift-enter .

    公式

    {=TRANSPOSE(MMULT(TRANSPOSE(((ROW(INDIRECT("1:"&e1))>=IF(COLUMN(set_of_pos)-COLUMN(INDEX(set_of_pos;1;0)=0;0;OFFSET(set_of_pos;0;-1))+1)*(ROW(INDIRECT("1:"&e1))<=set_of_pos))*((ROW(INDIRECT("1:"&e1))>=x2+1)*(ROW(INDIRECT("1:"&e1))<=x1)));--(ROW(INDIRECT("1:"&e1))>0)))}
    

    其中e1 是1 列数组中的元素数(也是最后一个位置的值),x1x2 是包含开始索引和结束索引的单元格的引用; set_of_pos 是 RANGE,表示每个部分中每个的最后位置(例如,对于 i03:2,9,18)

    零件 ((ROW(INDIRECT("1:"&amp;e1))&gt;=x2+1)*(ROW(INDIRECT("1:"&amp;e1))&lt;=x1))) 创建一个由连续数量的 1(来自布尔值)组成的(公式中)数组,以 0 为界,表示需要保留或注册的索引。 (尺寸 e1 × 1。)

    ((ROW(INDIRECT("1:"&amp;e1))&gt;=IF(set_of_pos=1;0;OFFSET(set_of_pos;0;-1))+1)*(ROW(INDIRECT("1:"&amp;e1))&lt;=set_of_pos)) 创建一个维度为 e1 × columns(set_of_pos) 的(公式中的)数组,其中每个部分单元格将值扩展为 1 的堆栈。 【这部分,对于每一列,上界是实际值,下界是左边的值加1——因为set_of_pos是一个RANGE,所以对set_of_pos中的每一列都是这样做的,只要用CSE进入公式;注意:如果是最左边的单元格,则使用 0 代替。]

    然后两个部分都用 * 操作,从而获得一个(公式中)维度为 e1 × columns(set_of_pos) 的数组,其位置满足在节堆栈中并且在要保留的索引集中(方便地分隔在列)。

    我们需要对这个结果进行转置,得到一个维度列(set_of_pos)×e1 的(公式中的)数组。 MMULT(以及由--(ROW(INDIRECT("1:"&amp;e1))&gt;0 定义的全1 e1 × 1)使我们能够计算压缩数组中每一列的1 的数量,其中的数量在不同的列[columns(set_of_pos) × 1] 中。 [注意,我们需要包含双负“--”来将布尔值转换为数字,因为 MMULT 需要数字数组,否则它会引发错误 - 根据函数规范,MMULT 不接受布尔值。]

    需要额外的转置才能将数组水平放置 [1 × columns(set_of_pos)]。

    现在,最终结果由=SUMPRODUCT(previous_result;set_of_val) 获得,其中previous_result 是上面的结果,set_of_val 是具有值的范围。

    有一种方法可以保存修改了哪些堆栈用于其他目的(可以计算花费的堆栈)并获得最终结果是函数 SUMPRODUCT 的简单应用。列或部分的数量不是硬编码的 - 它是通过在公式范围内选择 set_of_pos 并在应用 (CSE) 数组公式时选择适当的连续列数来设置的。

    方法 2 为了完整起见,已经探索了第二种方法。如果这被认为是较少的资源密集型,我当然想知道。我的预感是虽然打字更少,但它更耗费资源,但我无法对其进行测试。

    基本上,一个包含所有值的字符串被构建到一个单元格中(例如,使用 REPT 和管道 |,对于 i03,我们将有 "30.0|30.0|30.4|30.4|30.4|30.4|30.4|30.4|30.4| 30.2|30.2|30.2|30.2|30.2|30.2|30.2|30.2|30.2|")

    一个可行的解决方案(不需要 CSE - SUMPRODUCT 负责充分构建三个 1 列数组)

    =SUMPRODUCT(((ROW(INDIRECT("1:"& e1))<=x1)*(ROW(INDIRECT("1:"&e1))>=x2+1))*TRIM(MID(SUBSTITUTE(s1;"|";REPT(" ";LEN(s1)));(ROW(INDIRECT("1:"& e1))-1)*LEN(s1)+1;LEN(s1))))
    

    其中 x1、x2 和 e1 是包含开始索引和结束索引以及总元素数(如上定义)的单元格的引用,而 s1 是所有值按顺序排列的字符串。

    零件

    (ROW(INDIRECT("1:"& e1))<=x1)*(ROW(INDIRECT("1:"&e1))>=x2+1))
    

    本质上存在于方法 1 中:一个由连续数量的 1(来自布尔值)组成的(公式中)数组,以 0 为界,表示需要保留的索引。 (尺寸 e1 × 1。)

    TRIM(MID(SUBSTITUTE(s1;"|";REPT(" ";LEN(s1)));(ROW(INDIRECT("1:"& e1))-1)*LEN(s1)+1;LEN(s1)))
    

    该公式采用字符串 (s1) 并将每个分隔符替换为等于字符串长度的空格数,然后在由 ROW(INDIRECT("1:"& e1) 创建的数组的每个元素上放置不同的部分)

    [更通用的解决方案是通过获取字符串和去掉分隔符的字符串之间的差异来获得元素的数量,但是在我的小项目中需要元素的数量,并在标签 e1 下定义。该技术在别处进行了简明扼要的解释,将原始字符串的长度部分从 ROW(INDIRECT("1:"& e1)) times 给出的序列号开始,原始字符串的长度字符串,然后用 TRIM 删除所有周围的空格。]

    致谢和参考

    感谢答案和 cmets,可以找到两个具有相关 cmets 的 stackoverflow 问题:

    https://stackoverflow.com/questions/25316094/split-a-string-cell-in-excel-without-vba-e-g-for-array-formula/25316426[enter 此处链接描述][1] 和 Is there a way to concatenate two arrays in Excel without VBA?

    这两本书都不错。方法 1 部分是从头开始开发的,但方法 2 通过仔细阅读得到了极大的改进,特别是在将字符串转换为公式内数组方面。已选择 ROW(INDIRECT()) 而不是 SEQUENCE(),因为需要与 Excel 2010 兼容。

    【讨论】:

    • 我被私下要求使用最终版本,因为公式编辑很小。它可能会使公式稍微不那么清楚,但它几乎需要相同数量的解释。现在,它已更新。
    【解决方案4】:

    我会这样解决这个问题:

    =SUMPRODUCT((B2:D6*B10:D14)*(RIGHT(A2:A6)&gt;=RIGHT(X1))*(RIGHT(A2:A6)&lt;=RIGHT(X2)))

    SUMPRODUCT() 将允许我们作为数组进行评估,'=SUM()' + CSE 是同样有效的解决方案。

    (B2:D6*B10:D14) 是我们的数组。我浪费了很多时间试图强制数组评估 {20, 20, 20.6, 20.6, 20.6, 20.6},然后我才意识到我们只是要对整行求和。我不会详细介绍,但如果您想这样做,请从这里开始并使用 REPT 函数 (https://exceljet.net/formula/text-split-to-array)。

    (RIGHT(A2:A6)&gt;=RIGHT(X1))*(RIGHT(A2:A6)&lt;=RIGHT(X2)),因为最后一个字符是唯一的升序数字,如果 X1 中引用的结尾小于或等于行数,我就让它评估 TRUE()。当然,X2 的倒数。

    布尔 TRUE 和 FALSE 值可以应用代数,TRUE 为 1,FALSE 为 0。所以我只是将它们相乘以丢弃搜索区域之外的任何值。

    【讨论】:

    • 相信公式不会这样做,特别是如果约束适用于任意名称的 P1-P3 (在我看来,RIGHT(A2:A6) )。提出的两种方法非常值得回顾。 1-REPT Excel 函数启用字符串方法,但我会避免硬编码最大数量的组件。 2-SUMPRODUCT 方法,这将取决于开始完成将数组(对于 i03 {2;7;9})转换为另一个数组,如果第 3 到第 12,它将是 {0;7;3} 和这可以通过使用 x1 和 x2 来实现,这是一个可以更改范围的通用公式。如果我误解了,请道歉。
    • @user1938578,我认为你走在正确的轨道上,这正在推动可能的极限。但这是我最喜欢的问题,它推动了我学习新事物的极限。我误解了这个问题,我将把原来的“答案”留给后人。如果我以后发现更好的东西,我会通过编辑把它放在上面。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-05
    • 2023-02-09
    • 2021-11-12
    • 1970-01-01
    • 2021-06-27
    • 1970-01-01
    • 2018-09-05
    相关资源
    最近更新 更多