【程序设计】9.3 [分类别] 有序分组
9.3 有序分组
我们说过,排列、序列这些集合都是有序的,经常会有成员的位置信息参与到计算中。分组也不例外,分组键也有可能是和成员序号相关的。
比如,我们想把人员表每 3 个分成一组:
A |
|
1 |
=100.new(string(~):name,if(rand()<0.5,"Male","Female"):sex,50+rand(50):weight,1.5+rand(40)/100:height) |
2 |
=A1.group((#-1)\3) |
A2 中的分组时并没有使用 A1 中记录的任何信息,只是用了序号,这也是一种合理甚至还比较常见的分组。毕竟,分组的本质就是把一个大集合拆分成小集合,只要是有一种明确的拆分方法,那都算合理。
把人员表分成 3 组,可以用以前讲过的 step 函数,也可以直接用 group。
A |
|
1 |
… |
2 |
=A1.group(#%3) |
顺便复习一个,这两个例子的分组键值都是整数,还可以改造前面讲过的序号分组(要保证分组键值是从 1 开始的自然数):
A |
|
1 |
… |
2 |
=A1.group@n((#+2)\3) |
2 |
=A1.group@n(#%3+1) |
除了直接使用序号作为分组键值,分组还可能是相邻成员有关。
比如给定一个由字母和数字构成的字符串,要拆分成连续的字母和数字的部分,比如 abc1234wxyz56mn098pqrst,要拆分成 abc,1234,wxyz,56,mn,098,pqrst 这几个小字符串。
A |
|
1 |
abc1234wxyz56mn098pqrst |
2 |
=A1.split().group@o(isdigit(~)).(~.concat()) |
group@o 函数将依次扫描整个序列,当分组键值和上一个成员的分组键相同时,则将该成员加入到当前的分组子集,如果分组键值发生变化了,则产生一个新的分组子集并加入当前成员,扫描完之后就得到一批分组子集,从而完成分组运算。
isdigit(x) 当 x 是数字字符时返回 true,否则返回 false。A1.split()已经学过了,它会把这个字符串拆成单个字符构成的序列。这样,分组键值表达式 isdigit(~) 针对这个单字符序列会依次计算出
false fasle false true true true true false …
也就是碰到字母是 false,碰到数字是 true。从数字变到字母或从字母变到数字时都会引起 isdigit(~) 变化,从而产生一个新的分组子集,所以这里 group@o 的返回值将是
[[a,b,c],[1,2,3,4],[w,x,y,z],[5,6],….]
即把相邻的字符或数字分到了一组,然后再用 concat 把每一组拼成字符串就行了。
这种分组用前面学过的分组运算很难描述。
这还是个直接针对序列做分组的例子。
如果我们知道分组键值本身有序,常规的等值分组也可以用 @o 来做。比如订单数据如果对日期有序时,我们想按月分组汇总时,可以这样写:
A |
|
1 |
=T("data.xlsx") |
3 |
=A2.groups@o(month@y(OrderDate);sum(Amount),count(Amount)) |
@o 对 groups 也有效。在这里是否使用 @o 的结果没有区别,但有序时使用 @o,groups 运算时只需要和相邻的成员比较分组键值,计算速度要快得多。
@o 还有一个变种 @i,这时候分组键时是个逻辑表达式,每当计算出 true 时则产生一个新的分组子集。
我们回顾一下之前做过的计算股票最长连续上涨天数的问题。可以换一种思路,将交易数据按日期排序(经常本来就是有序的),然后依次扫描这些数据进行分组,如果某天价格比前一天上涨了,则将这一天继续和前一天分到同一组;如果没上涨,则产生一个新的分组。等扫描完之后会得到一些分组子集,在每个子集中股价都是连续上涨的,然后我们只要看哪个成员最多的子集就行了。
A |
|
1 |
=100.new(date(now())-100+~:dt,rand()*100:price) |
2 |
=A1.select(day@w(dt)>1 && day@w(dt)<7) |
3 |
=A2.group@i(price<=price[-1]).max(~.len()) |
顺便提一下,SQL 要完成这个任务,目前找到的最简单的办法也是使用这个思路,但写出来的代码是这样的(只对应 A3 格的代码):
SELECT max(consecutive_day)
FROM (SELECT count(*) consecutive_day FROM
(SELECT sum(rise_or_fall) OVER(ORDER BY dt)
day_no_gain FROM
(SELECT dt, CASE
when price>lag(price)
OVER(ORDER BY dt)
then 0 else 1 end rise_or_fall
FROM T ) )
GROUP BY day_no_gain)
可以感受一下。
需要利用次序和位置信息的分组运算称为有序分组,有序分组在解析文本时非常有用。
有些文本文件并不是非常整齐的 txt 或 csv 格式,而是有一定规律的文字串,其中会有结构化数据,想再次利用时还需要编程来解析,比如类似这样的:
一般来说,日志中会描述一批事件,每个事件会占用几行文本。结构化解析,就是把每个事件的字段取值抽取出来。
使用有序分组运算可以方便地把这些日志文本拆分成事件为单位的小段。日志划分事件通常用这行几种方式:
1)固定每 N 行对应一个事件,可以用 group((#-1\N)) 办法拆分:
A |
B |
|
1 |
=file("S.log").read@n() |
|
2 |
=create(…) |
|
3 |
for A1.group((#-1)\3) |
… |
… |
… |
|
… |
>A2.insert(…) |
2) 行数不固定,每个事件前会有个固定起始字符串,可以使用 group@i 拆分:
3 |
for A1.group@i(~=="---start---") |
… |
3) 行数不固定,同一个事件的每行有个相同的前缀(比如该段日志所属的用户号等),可以使用 group@o 拆分:
3 |
for A1.group@o(left(~,6)) |
… |
拆分之后,代码可以专心从每一段的字符串中解析出字段值。当然,这通常也不是个简单任务。