Power Query 学习 03 如何系统学会 List.Accumulate
前言:List.Accumulate
是M语言里,非常常用的函数。施神的帖子已经有了比较详细的论述,https://pqfans.com/1897.html ,如果你已经完全理解掌握了此帖,本文就不用看了。如果你看不懂大神的帖子,或看懂了,但自己写不出来,或写不顺手总写错。本文将提供一个比较系统的问题解决思路,来帮助大家用好这个函数。
正文:List.Accumulate
之所以常用,原因是它的功能是循环迭代。M是函数语言,里没有for
语句,只能用List.Transform,List.Accumulate等函数实现循环功能。不掌握这个函数,就相当于使用M语言却不能使用循环,结果可想而知!函数语言的优点是简洁,但是由于参数位的限制,导致函数语言的参数编写不够灵活。List.Accumulate
就是典型的,需要提前构造好第二参数(需要迭代的变量初值)的数据结构,难就难在这里!So,开始:
案例1:从 1 到 9 累加
为了便于理解,这里用Python
作对比,以帮助初学者理解函数式语言。
先看两种代码(请仔细对比代码区别,以便理解M语言的语法特点):
Python:
s=0
for i in range(1,10): #Range取值左闭右开,所以1到9要表达为(1,10)
s+=i #等效于s=s+i
M:
= List.Accumulate({1..9},0,(s, i)=> s+i) //还是很简洁的,就是这个函数名,太长了。。。
简单解读一下:
变量 s 为累加器,所以要放在循环外边定义,并附初值 0
变量 i 为循环变量,随着每次循环,i 的值依次取 1 到 9
循环表达式:s=s+i ,如此循环 9 次:
s1=s0+i0 0+1=1
s2=s1+i1 1+2=3
s3=s2+i2 3+3=6
s4=s3+i3 6+4=10
...
s9=s8+i8 36+9=45
下面我们来看正式看List.Accumulate
的语法说明和我的“翻译”
List.Accumulate(list as list, seed as any, accumulator as function) as any
List.Accumulate(要循环的列表, 迭代初值,(迭代变量名,循环变量名)=> 循环运算表达式)
这里要注意迭代初值的数据类型为any
,这里既可以是数字、文本,也可以是列表、记录甚至表都可以。
另外,如果需要的结果是整个列表,只是按一定规律的转换一遍,应该用List.Transform
更加合适,当然用List.Accumulate
也可以实现,比如下面两行代码是等效的:
= List.Transform({1..9},each _*2)
= List.Accumulate({1..9},{},(x,y)=> x&{y*2})
好,如果你理解了案例1,并能自己写出一个 1 到 9 阶乘,就继续往下:
案例2:把 1 到 9 按小于 5 和大于等于 5 分成两组
先看Pythom代码:
l1,l2=[],[]
for i in range(1,10):
if i<5:
l1=l1+[i]
else:
l2=l2+[i]
原理很简单,定义两个空列表,小于5装第一个,否则装第二个。
可这会儿我们要改写成List.Accumulate
时就出问题了,因为只有一个第2参数,没法放进 l1、l2 这 2 个列表,那能不能把两个迭代变量改成一个呢?当然可以,只要把两个列表组合成一个就行了。下面是组合后的Python
和M
代码:
Python:
l=[[],[]]
for i in range(1,10):
if i<5:
l=[l[0]+[i],l[1]]
else:
l=[l[0],l[1]+[i]]
M:
= List.Accumulate({1..9},{{},{}},(x,y)=>
if y<5 then
{x{0}&{y},x{1}}
else
{x{0},x{1}&{y}})
很容易发现,只要把要迭代的变量,组合成一个列表List
,就可以。当然组合成记录Record
,甚至组和表Table
都行,相应的修改后面的函数表达式就可以了。此处用List
比较简洁,用Record
比较易读,没有本质区别,比如下面两段代码,作用是等效的:
= List.Accumulate({1..9},{{},{}},(x,y)=>
if y<5 then
{x{0}&{y},x{1}}
else
{x{0},x{1}&{y}})
= List.Accumulate({1..9},[a={},b={}],(x,y)=>
if y<5 then
[a=x[a]&{y},b=x[b]]
else
[a=x[a],b=x[b]&{y}])
本文为了代码相对简化,案例中List.Accumulate
的第 2 参数,统一使用List类型。
好,再举两个例子,熟悉一下构造和条件写法,这两个案例都是把数字和字母分开,但其中一个是文本型数字,所以条件的写法不同,一个是直接判断数据类型,另一个则是用比较字符大小,来判断字符类型(可参考ASCII编码表):
如果这些都看懂了,并且学会了,下面继续,由于对函数构造有了一定了解,后面就不再写对比Python
代码。
案例3:分组相同的字母
就是要把"AABBBBCCC"
,分成{"AA","BBBB","CCC"}
。分析一下这个题:
首先需要把文本拆成列表,然后逐个进行前后比较,相同的合并成一组,不同另起新组并把合并好的放入结果组。这个有了前后比较关系的题目,明显比前面的问题更棘手。要想实现这个功能需要解决如下问题:
1、前后比较,也就是需要一个 容器 每次需要保留当前字母,以便和下一个比较
2、需要有一个 容器 ,来存放合并的文本
3、需要一个 容器 ,来存放最终结果列表
4、首次和末次比较,需要特殊处理
5、深化引用列表首项得到最终结果
接下来我们来构造第 2 参数,为了满足前 3 个问题,就需要构建由 3 个容器组成一个List
,模式如下:
{ 最终列表 , 合并文本 , 保留字母 } 再根据数据类型,可确定 2 参数为 { {} , "" , "" }
下面再看比较逻辑:
如果 保留字母=新字母,那么:
最终列表:不变
合并文本:加入新字母
保留字母:变为新字母
如果 保留字母<>新字母,那么:
最终列表:加入 合并文本
合并文本:变为新字母
保留字母:变为新字母
根据以上内容,我们写出第 1 版代码:
let
源 = List.Accumulate(Text.ToList("AABBBBCCC"), {{},"",""},(x,y)=>
if x{2} = y then
{x{0} ,
x{1}&y ,
y }
else
{x{0}&{x{1}} ,
y ,
y }
){0}
in
源
验证一下:
发现结果不对,前面多了个空文本,后面少了一组,下面逐步分析一下迭代过程变化过程:
{{},"",""} and "A"
{""},"A","A"} and "A"
{""},"AA","A"} and "B"
{"","AA"},"B","B"} and "B"
{{"","AA"},"BB","B"} and "B"
{{"","AA"},"BBB","B"} and "B"
{{"","AA","BBBB"},"BBBB","B"} and "C"
{{"","AA","BBBB"},"C","C"} and "C"
{{"","AA","BBBB"},"CC","C"} and "C"
通过分析不难发现,原来刚才的问题4、首次和末次比较,需要特殊处理
还没处理,那怎么办呢?
第一次比较,因为初始值是写好的,我们知道,那就增加一个判断专门处理
最后由于缺少一次比较,我们在列表最后增加一个元素,来完成处理(要能确定这个元素和列表里的都不同)
升级后的代码如下:
let
源 = List.Accumulate(Text.ToList("AABBBBCCC")&{null}, {{},"",""},(x,y)=> //列表最后增加了一个null
if x{2} = "" then //首次比较处理条件
{x{0} ,
y ,
y }
else if x{2} = y then
{x{0} ,
x{1}&y ,
y }
else
{x{0}&{x{1}} ,
y ,
y }
){0}
in
源
验证一下,结果终于对了:
再来一个连续数分组,{1,2,4,6,7,8}
变为{{1,2},{4},{6,7,8}}
思路完全一样,先构建第 2 参数:
{ 最终列表 , 每组连续数 , 用于比较的数字 } ,再根据数据类型确定为 {{},{},""}
这里每组连续数
类型变为List
,后面的操作也都做了相应改变,注意和上一个案例的细微区别:
let
源 = List.Accumulate({1,2,4,6,7,8} & {null}, {{},{},""},(x,y)=> //列表最后增加了一个null
if x{2} = "" then //首次比较处理
{x{0},
{y},
y }
else if x{2} = y-1 then
{x{0} ,
x{1}&{y} ,
y }
else
{x{0}&{x{1}} ,
{y} ,
y }
){0}
in
源
上面都学会那继续增加难度:
案例4:连续字母合并,连续数字相加,然后分到一组
也就是"A345BC222WWW012"
变为:{{"A",12},{"BC",6},{"WWW",3}}
,WTF,这个要怎么搞?
其实,思路还是一样的!
1、前后比较,也就是需要一个 容器 每次需要保留当前字符,以便和下一个比较
2、需要有一个 容器 ,来存放合并的文本
3、需要一个 容器 ,来求和数字
4、需要一个 容器 ,来存放最终结果
5、首次和末次比较,需要特殊处理
6、深化引用列表首项得到最终结果
其实就是多了一个中间容器吗。我们来构造第 2 参数如下:
{ 最终列表 , 连续字母合并 , 连续数字求和 , 用于比较的字符 } ,再根据数据类型确定为 {{},"",0,""}
同样变复杂的还有前后比较的条件:
1、首次条件 : 写入字母
2、字母 字母 : 合并字母
3、字母 数字 : 写入数字
4、数字 数字 : 加数字
5、数字 字母 : 写入合并的字母和数字的和,写入字母,数字清零
通过分析我们可以合并化简3\4条,并需要在最后补一个字母,最终代码如下
let
源 = List.Accumulate(Text.ToList("A345BC222WWW012") & {"A"}, {{},"",0,""},(x,y)=> //列表最后增加了一个"A“
if x{3} = "" then //首次比较处理
{x{0} ,
y ,
x{2} ,
y }
else if x{3}>="A" and y>="A" then //条件2
{x{0} ,
x{1}&y ,
x{2} ,
y }
else if y<"A" then //条件3、4
{x{0} ,
x{1} ,
x{2}+Number.From(y) , //相加之前要先转为数字格式
y }
else //最后一个条件省略
{x{0}&{{x{1},x{2}}} , //注意列表层数
y ,
0 ,
y }
){0}
in
源
敲黑板,总结!
1、List.Accumulate 相当于 M语言 的 For 循环,1\2\3参数为:循环列表(List),迭代初值(Any),循环表达式(function)
2、需根据题目具体需求,设置第 2 参数,较复杂问题使用 List 或 Record 均可
3、前后比较分组问题,第 2 参数构建模型为:{ 最终列表容器 , 过程容器1 , 过程容器2... , 比较容器 }
4、前后比较分组问题,条件书写通用模式为:(x,y)=> if (x{n}与 y 的各种属性和关系判断) then {x{0} , x{1} ... , y }
5、前后比较分组问题,需考虑首次比较和末次比较处理,一般首次用条件处理,末次用补值处理,补值类型要满足逻辑需求
砸黑板,作业!
自我检测一下呗:
用List.Accumulate把 "AAbb123BBcc234" 转换成如下结果:
1、{"AAbbBBcc","123234"} //20分
2、{"AAbbBBcc",15} //40分
3、{"AAbb123","BBcc234"} //60分
4、{{"AAbb","123"},{"BBcc","234"}} //80分
5、{{"AA","bb",6},{"BB","cc",9}} //100分
看到这都没放弃啊。。。
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)