List.Generate

官方说明:

给定生成初始值initial的四个函数,针对条件condition进行测试,如果成功,则选择结果并生成下一个值next,以此生成值列表。还可以指定可选参数selector。
List.Generate(initial as function, condition as function, next as function, optional selector as nullable function) as list

解读:

一般来说,函数中只要有一个类型是function的参数,就可以无限延伸扩展,函数套函数,就会比较难。
今天要讲的这个函数,一共4个参数,但4个参数全部都是function你见过没?
个人认为这个函数难度排在所有函数的top3,所以如果你确定要继续往下看,请先弄懂前面讲的List.TransformManyList.Accumulate,否则。。。
我的理解:List.Generate(()=>初始赋值,列表限定条件,迭代的自定义运算,可选择自定义输出迭代的传输变量)

先看官方给的两个案例:
= List.Generate(()=>10, each _>0, each _-1)
第一参数初始值为10,传递到第二参数进行判断是否>0,如果成立,再传递到第三参数减1,第一步结果为9。
再把9传递到第二参数继续判断,每次成立就传递到第三参数,每次减1。
直到减到结果为1,经过第三参数1-1=0,再传递到第二参数判断,0不大于0,跳出循环。
最终返回前面所有符合条件的值的列表,结果为{10,9,8,7,6,5,4,3,2,1}。
注意,虽然第一步的结果就是9,但最终的结果会包含初始值的10,这是和acc有点区别的地方。

如果说acc相当于for each in next,那么List.Generate则相当于while循环。
打个比方,手机卡充值了100元,打电话2毛一分钟,每拨出一个电话,系统判断余额是否>0,如果是,接通电话,并且每分钟扣掉2毛。一旦余额<0,对不起,您的电话已停机。 最后你跑到营业厅把通话详单打出来,上面显示每通电话扣了多少钱,余额还有多少。
 
还有个可选的第四参数,来看第二个例子:
= List.Generate(()=>[ x = 1 , y = {}] , each [x] < 10 , each [x = List.Count([y]), y = [y] & {x}] , each [x])
初始值为[x=1,y={}],第一步x<10成立,执行第三参数,结果为[x=0,y={0}]。 后面的步骤只要x也就是y中元素个数<10,就循环一次,而每次循环y中就多一个元素,x就会+1,每步的结果都是record,但是第四参数限制了只输出x,所以最后的结果就是初始的1和0-9组成的list。就是说我办的是家庭套餐,消费记录可能还有其他号码的,但加上第四参数我就只看我自己的号码。  
 
同样,初始值由一个无参函数()=>生成的值,不仅可以是上面的number,record,也可以是list等其他任意类型。
比如要构建一个等差数列,初始值为1,公差为2,列出数列的前9项:
= List.Generate(()=>{1,0},each _{1}<9,each {_{0}+2,_{1}+1},each _{0})
初始值为{1,0},1为数列的首项,0用来记录循环次数也就是数列的项数。
每循环一次,首项+2,项数+1,当项数不满足<9时跳出循环,第四参数_{0}控制只输出第一个元素,最终结果为{1,3,5,7,9,11,13,15,17}。
 
再来一个简单的案例:指定一个日期,列出从这一天开始到当月最后一天的所有日期。
= List.Generate(()=>#date(2017,8,17),each _<=Date.EndOfMonth(#date(2017,8,17)),each Date.AddDays(_,1))
初始值为该日期,每次循环先判断是否<=当月最后一天,满足就加一天,不满足则跳出循环返回所有符合条件的日期列表。 看到这我们可以大致总结出,List.Accumulate侧重于迭代的最终结果,而List.Generate偏向迭代中每一步的过程。这句话的另一个意思就是,只要取generate结果的最后一个值,就是acc的结果。所以大多数情况下,acc能做的,generate都能实现,只不过是用谁更方便的问题。

比如之前用acc实现的批量替换文本:
= List.Last(List.Generate(()=>[x="上海市浦东新区",y=0],each [y]<=3,each [x=Text.Replace([x],{"上","东","新"}{[y]},{"下","西","旧"}{[y]}),y=[y]+1],each [x]))
x为需要替换的文本,y用来循环索引,初始值为0。第二参数中的3为待替换列表中元素的个数,每一步替换完一个值,y+1,下一步替换下一个元素,直到替换完列表中所有的值。
最终得到初始值和三次替换中每一次的结果列表,再用List.Last取最后一个值。

再比如累计求和:
= List.Skip(List.Generate(()=>[l={1,2,3,4,5},i=0,r=0],each [i]<=5,each [l=[l],i=[i]+1,r=[r]+[l]{[i]}],each [r]))
初始值中,l表示需要累计求和的list,i表示循环索引index,r为result用来装载结果,第二参数中的5为l中的元素个数。
i从0开始,每一步取l中第i个元素,从0开始累加并记录到r中,每次循环i+1,第四参数控制只输出r。
最终返回的结果中包含了初始值0,再用List.Skip跳过初始值。

当嵌套层数比较多时,第一参数都建议使用record而不用list,并且变量名尽量起有意义的名字,否则很容易把自己绕晕。
以上的案例虽然generate也能做,但显然是acc更方便一些,下面看几个generate为最优解的案例:

进制转换


先了解下十进制转二进制的计算过程:将十进制数不断除以2取余数,直到取整的结果=0,最后将余数取反,翻译为M语言就是:

= List.Reverse(
	List.Generate(()=>
		25,
		each _<>0,
		each Number.IntegerDivide(_,2),
		each Number.Mod(_,2)
	)
)

 

提取1000以内质数

= {2}&List.RemoveNulls(
		List.Generate(()=>[
			n=3,
			m={}
		],
		each [n]<1000, each [ n=[n]+2, m={2..n-1} ], each if List.AnyTrue(List.Transform([m],(x)=>Number.Mod([n],x)=0)) then null else [n]
	)
)

质数为在大于1的自然数中,除了1和它本身以外不再有其他因数。除了2以外,所有的质数都是奇数,但是奇数不一定是质数,比如9=3*3。
整个过程就是循环遍历,把1000以内的所有奇数从3开始,挨个用2..n-1除,判断余数是否为0,返回一个布尔值的列表。
如果列表中有任意一个为true就返回n,否则返回null。
比如5除以2-4都不符合,就返回5。9除以2-8中,9除以3等于3余0,就返回null。
最后移除null,再连接上特殊值2。

约瑟夫环

把13个人围成一圈,从1开始报数,报到4杀掉然后下一位接着从1开始报,剩最后一人为幸存者。
要求列出每一轮被杀死的人及最后的幸存者。
之前在acc中给了一个数学公式推导的解法,这里再给一个generate的模拟解法。

= List.Skip(
	List.Generate(()=>[
			l = {1..13},
			k = List.Count(l),
			n = 1,
			m= l{n-1}
		]
		,each [k]>=0
		,each [
			l = List.RemoveItems([l],{m}),
			k = [k]-1,
			n = [a=Number.Mod([n]+3,[k]),b=if a=0 then [k] else a][b],
			m = [l]{n-1}
		],
		each [m]
	)
)

讲这个问题之前,我们先来看一个简单的问题:一组连续的k个数{1..k},报任意一个数n,从1开始数到k,然后再从头开始接着数,求数到n时是第几个数。
比如k=3,n=8,那么依次是1,2,3,1,2,3,1,2,数到8时为2。
这很容易理解,每k个数分为一组,用n除以k取余数即为结果。
但是如果n是k的倍数,比如k=3,n=6,取余数为0,那结果应该是k。以上翻译为M语言就是:
= (n,k)=>[a=Number.Mod(n,k),b=if a=0 then k else a][b]
公式可以进一步推导简化,写成:
= (n,k)=>Number.Mod(n-1,k)+1
如果不理解的话脑补一下画"正"字计数法。我们暂且先用上面那个容易理解的,不论n大于还是等于或是小于k,公式都同样适用。

再回来看这个问题,首先从刚开始,每个人身上贴一张从1-13编号的铭牌,这个编号是从开始到结束都不变的,我们记为m。
第一轮报到4的是4号,杀死出局。下一个报到4的是8号,但是4号已出局,目前场上还有12人,从1号开始数现在8号是第7个,这个7是序号,每过一轮都会变一次,我们记为n。
l表示list,为所有未死亡的人的列表,每过一轮少一人,即每次循环使用List.RemoveItems把死亡的人移出队列;k表示剩余人数,每次循环-1。
先假设现在不是13人而是100人站一条直线,报到4的人叫一声但不出局,开始报数的是1,报到4号叫一声,然后从5开始报1,下一个叫的人是8号,m依次为4,8,12,16...
但是现在规则是报到4的人被杀死则出局,每报一轮就少1人,所以n应该为4,8-1=7,12-2=10,16-3=13...是一个首项为4的等差数列,公差为3。
那么现在问题就可以简化成现有k个人,求报到n时是第几个人,直接套用上面的公式。每过一轮n+3,k-1。

整个过程就是,先判断剩余人数k是否>=0,如果满足,每次循环先把已死亡的人移出队列l,然后人数k-1,再找到报4人的序号n,深化出{n-1}个即为本轮死亡人的编号m。
当人数k<=0跳出循环,第四参数控制输出m,最后再用List.Skip跳过初始值,返回最终结果。

总结:

这是一个非常灵活但同时也非常难的函数,相当于while循环。如果没有其他函数,可以用它来模拟替代很多其他函数做的事。
但如果是为了解决一个实际问题,毕竟还有很多函数可以选择,所以我的理解是generate虽然强大,但是不到万不得已不要用。
本来还准备了很多其他案例,但因为不是最优解所以没放上来。
以上案例大多都是算法题,最后再给一个实际应用的案例《BOM树形结构分解》

12 Replies to “List.Generate”

  1. = List.Generate(()=>[ x = 1 , y = {}] , each [x] < 10 , each [x = List.Count([y]), y = [y] & {x}] , each [x])
    初始值为[x=1,y={}],第一步x<10成立,执行第三参数,结果为[x=0,y={1}]。 结果应该为[x=0,y={0}] 。笔误了。

  2. "如果列表中有任意一个为true就返回n,否则返回null。",这句话的结论和本意不符,写反了,应为:任意为true就是合数,返回null,否则,返回n。

  3. 我一直想把代表十六进制的多字符串转换为十进制,看到这个似乎有了点希望,但思路还是理不顺,博主麻烦您指导指导吧

  4. = List.Generate(()=>[ x = 1 , y = {}] , each [x] < 10 , each [x = List.Count([y]), y = [y] & {x}])

    把List.Count(y)中的 [y] 改为 y
    一样能计算,但是实在不知道运算规则
    个人觉得一个record内这样就重复引用了,但是却能出结果,百思不得其解
    求教大神

    1. 刚在群里学到的,把List.Count(y)中的 [y] 改为 y后,之所以没有出现死循环,是因为x无论怎么变,他都只能是个数字,所以,List.Count(y)只能有一个结果,那就是1。

  5. = List.Skip(List.Generate(()=>[l={1,2,3,4,5},i=0,r=0],each [i]<=5,each [l=[l],i=[i]+1,r=[r]+[l]{[i]}],each [r]))
    累计求和过程,很容易搞混的是 r=[r]+[l]{[i]},里面 i 的数据来源 应该是 上一步时 i 的值,而不是 本步骤下 i的值。
    例如i,r初始都为0, 第一步:得到{1..5},i=0+1, r=0+{1..5}{[i=0]}里面这个i是上一步的0,而不是本步的1

  6. 您好,关于约瑟夫环(最后一个案例),其中list中第一个元素1,在第一轮就已经被移除了,为什么后续还能继续出来呢?

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注