Excel 公式部分从 table 数据动态构建数组

Excel formula section to build an array from table data dynamically

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

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

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

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

例如:

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。

我发现很难从公式中的 table 构建数组,尤其是因为每个标识符可能具有不同数量的组件。

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

以下是我的解决方法:

=SUMPRODUCT((B2:D6*B10:D14)*(RIGHT(A2:A6)>=RIGHT(X1))*(RIGHT(A2:A6)<=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)>=RIGHT(X1))*(RIGHT(A2:A6)<=RIGHT(X2)),因为最后一个字符是唯一的升序数字,如果 X1 中引用的末尾小于或等于行的数字,我就让它计算 TRUE() 。当然还有 X2 的逆。

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

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

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

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)))

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

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

方法 1(首选)

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

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

其中 e1 是 1 列数组中的元素数,lb1 是需要 1 的下限,ub1 是上限,out边界元素的值为 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 -输入。

公式

{=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:"&e1))>=x2+1)*(ROW(INDIRECT("1:"&e1))<=x1))) 创建一个(公式中的)数组,其中包含连续数量的 1(来自布尔值),以 0 为界,表示需要保留或注册的索引。 (尺寸 e1 × 1。)

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

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

我们需要转置此结果以获得维度列 (set_of_pos) × e1 的(公式内)数组。 MMULT(以及 --(ROW(INDIRECT("1:"&e1))>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) 数组公式时选择适当的连续列数来设置的。

方法二 为了完整起见,探索了第二种方法。如果这被认为是较少的资源密集型,我当然想知道。我的直觉是,尽管打字更少,但它占用的资源更多,但我无法对其进行测试。

基本上,一个包含所有值的字符串被内置到一个单元格中(例如,使用 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 可以充分处理三个单列数组)

=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.]

删除所有周围的空格

致谢和参考文献

感谢回答和评论,找到了两个具有相关评论的 Whosebug 问题:

link 说明][1] 和

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

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

通用表达式

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

转换为

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

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

OFFSET 是唯一的其他可变函数,使用了一次(从左侧的单元格获取下限)。可以换成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:INDEX(:;0;COLUMNS(set_of_pos)))=1;0;INDEX(set_of_pos;0;COLUMN($A:INDEX(:;0;COLUMNS(set_of_pos)))-1))+1

]

其中 set_of_pos 是水平范围,表示原始 post 每个部分的最后位置(因此,当它是第一个 - 最左边 - 单元格时,公式部分给出 0+1和 1 + 左侧单元格的值,从而获得该部分的下限)。

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

{=TRANSPOSE(MMULT(TRANSPOSE(((ROW($A: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:INDEX($A:$A;e1;0))<=set_of_pos))*((ROW($A:INDEX($A:$A;e1;0))>=x2+1)*(ROW($A:INDEX($A:$A;e1;0))<=x1)));--(ROW($A:INDEX($A:$A;e1;0))>0)))}

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

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

上找到的 ROW(INDIRECT()) 的非易失性方法[搜索“Without the volatile”,Scott Craner 的评论]。