【程序设计】7.3 [字与时] 日期与时间

 

7.3 日期与时间

与时间相关的处理和计算也很常见,SPL 有三种数据类型:日期时间日期时间

日期数据就只有日期信息,没有时间信息;时间数据只有时间信息,没有对应的日期信息;而日期时间则是即有日期也有时间,也就相当于某个时刻了。

提供没有日期信息的时间类型是有必要的,有些在不确定日期发生的事情确实没有日期信息,用时间类型来表示就比较合适,一定要多个日期还可能出现混乱。比如计算 8 点和 9 点之间隔了多久,如果必须有日期信息时就要注意是不是同一天的 8 点和 9 点,这很容易被遗漏

但有了更精确的日期时间类型,为什么还要有个不太精确的日期类型?

这个道理和整数与浮点数差不多,因为对于很多事件只关心发生的日期而不再关心时刻了。都用日期时间类型,存储量和计算量都变大。而且,在比较两个日期是否相等时还要注意附带的时间信息是不是也相等。

不是所有程序语言都提供了这三种数据类型,大部分数据库都只有日期时间而没有日期。

日期时间相关的最基本运算就是分量的拆分及组合:


A

B

C

1

2020-12-20

=date(2020,12,2)


2

=year(A1)

=month(A1)

=day(A1)

3

22:3:5

=time(22,3,5)


4

=hour(A3)

=minute(A3)

=second(A3)

5

2020-12-2 10:3:5

=datetime(2020,12,2,10,3,5)

=datetime(A1,A3)

6

=year(A5)

=month(A5)

=day(A5)

7

=hour(A5)

=minute(A5)

=second(A5)

8

=date(A5)

=time(A5)


这些函数都很简单,看看示例就知道了,不必详细解释了。

SPL 还支持把年月放在一起作为一个分量,也就是 6 位数。


A

B

C

9

=date(202012,20)

=month@y(A9)


有时候会发现这样的处理会非常方便。

日期时间和字符串之间的转换要比较麻烦,会有非常多种类的格式。比如 2020-12-20,也可能写成 2020/12/20,12/20/2020,20-12-2020,欧美人甚至还会用 Dec/20/2020 或 20-Dec-2020 这种格式。时间相对少一点,但也可能写成 10:03:05 PM 或 22:3:5。

所以,能够处理日期时间的程序语言通常要提供这种格式解析和转换的能力。


A

B

1

2020-12-2

22:3:5

2

=string(A1,"yyyy/MM/dd")

=string(B1,"hh:mm:ss")

3

=string(A1,"MM/dd/yyyy")

=string(B1,"HH:m:s")

4

=string(A1,"MMM-d-yy")

=string(B1,"h:m:s a")

5

=string(A1,"d/MMM/yyyy")


6

=date("2020-12-20")

=time("22:3:5")

7

=date("DEC/20/20","MMM/d/yy")

=time("10:3:5 pm","h:m:s a")

8

=datetime(A1,B1)

=string(A8,"MM mm")

这些格式串的写法可以在函数帮助中查到,几乎是全世界都通用的规则,这里也不做详细解释了,只要知道这种用法就行了。需要强调的是,因为月份和分钟的英文单词起始字母都是 m,所以要用大小写做区分,不能写错了,可以观察 B8。

顺便提一句,其实数值也有格式的问题,


A

B

1

12345.23456

12345678

2

=string(A1,"#.00")

=string(B1,"#")

3

=string(A1,"#.0")

=string(B1,"#,###")

4

=string(A1,"#,###.0000000000")


这些也不详细解释了,自己尝试一下即可。

我们前面说过,计算机里其实只有数,其它数据类型实质上是某种编码下的数,比如 SPL 中字符串是用 unicode 编码的,那么日期时间又是怎么编码的?

日期和时间数据类型的内部编码都是长整数,我们来观察它的规律:


A

B

C

1

2020-12-1

2020-12-2


2

=long(A1)

=long(B1)

=B2-A2

3

0:0:0

0:0:1


4

=long(A3)

=long(B3)

=B4-A4

5

=date(0)

=time(0)


先观察 C2,两个相差 1 天的日期,转换成长整数后相差 86400000,这是个什么数?

经常做日期计算的读者会敏感地发现,86400=24*60*60,这正好是一天的秒数的 1000 倍。再看 C4,两个相差 1 秒的日期,转换成长整数后相差 1000。这证实我们的想法,日期时间对应的长整数是从某个时刻开始的毫秒数。

那么,从哪个时刻开始呢?直接看 A1、B1、A4、B4 是搞不清的,那么干脆反算一下,看 A5 和 B5。原来,日期是从 1970 年 1 月 1 日开始数的,这是计算机国际标准组织的一个约定,比 1970 年更早的日期就要用负数表示了。有意思的是,历史上有几天并不是精确的 86400 秒,某个日期对应的长整数值也不总是距离 1970 年 1 月 1 日的天数乘 86400000,有兴趣的读者可以编程寻找一下是哪些天(我们现在学会的技能已经完全能做这件事了),再去网上搜一搜看为什么这些日期不是 86400 秒。

再看时间,B5 结果比较奇怪,我这里算出来是 8 点,为什么不从 0 点开始?

因为我现在是在中国北京编写这本书,北京所在的时区是东 8 区,而北京时间 8 点正好是格林威治时间的 0 点。时间还是从 0 点开始数的,只不过是用的格林威治时间,而不是当地的时区,如果读者恰好现在在英国,那算出来的 B5 就会是 0 了。

日期时间还有这么多有趣的事情。

我们继续来学习日期时间有关的运算。最常见的需求就是计算两个时刻之间的差距以及从某个时刻过了一段时间后的时刻。

比如,我们想看看当初做水仙花数时采用的优化手段有多大效果,那么就要计算这个程序的执行时间:


A

B

C

D

E

F

1

=now()






2

for 1000

=0

=[]




3


for 9

=100*B3

=B3*B3*B3



4



for 0,9

=C3+10*B4

=D3+C4*C4*C4


5




for 0,9

=D4+D5

=E4+D5*D5*D5

6





if E5==E5

>C2.insert(0,E5)

7

=now()

=interval@ms(A1,A7)


这里我们要使用生成结果序列的代码,因为输出方法会涉及到屏幕显示,这个动作事实上非常复杂,很可能耗用的时间比完成这些计算还长,这就不是测试计算时间了。而且,我们把这个动作重复了 1000 遍,因为现代计算机的 CPU 实在太快,只执行 1 遍根本看不出差距。

A1 的 now()将返回执行这个语句的时刻,即把程序开始的时刻记入 A1。到 A7 再用 now() 得到程序结束的时刻,然后用 inteval@ms() 计算这两个时刻之间的差距,两个参数就按时间的次序写,这样就得到了这个程序执行的时间。

now()返回的是日期时间类型,它能精确到毫秒。interval() 的选项 @ms 表示计算时间差距时精确到毫秒。因为程序执行得很快,即使重复 1000 遍,如果不精确到毫秒,也仍然看不出差距。

现在把优化过的代码做一下同样的改造并执行:


A

B

C

D

E

F

G

1

=now()







2

for 1000

=0

=[]





3


for 9

=100*B3

=B3*B3*B3




4



for 0,9

=C3+10*B4

=D3+C4*C4*C4


break

5




for 0,9

=D4+D5

=E4+D5*D5*D5


6





if E5==E5

>C2.insert(0,E5)


7





else if E5<F5

break


8

=now()

=interval@ms(A1,A8)



对比这两个时间差距,就知道优化是不是起了作用。我的机器上,优化代码大概快了 25%,还是比较明显的。

interval 函数有各种选项,能返回不同精确度的时间差距。在日常工作中,我们最常见的还是计算两个日期之间的天数,这也是 interval 函数不用选项时的缺省返回值。而且,因为太常用了,SPL 把这种计算简化成直接用减法表示了,即 interval(d1,d2)==d2-d1。

某人在 2018 年 12 月 4 日借了 25000 元钱,每日利息为 0.013%,如果他今天归还,那么需要支付多少利息。


A

1

=25000*0.013/100*interval(date(2018,12,4),now())

2

=25000*0.013/100*(now()-date(2018,12,4))

A1 和 A2 这两种写法的结果是一样的。now() 包含有日期信息,可以当今天用。

情况再复杂一点,如果每年末要把利息加入本金再一起计息,那么到今天归还时应该总共支付多少钱呢?

也就是说每年 1 月 1 日时,要把当时的利息计算出来后加入到本金再去重新计算利息。我们用个笨办法来算,从借款日开始一天一天循环到今天。


A

B

C

D

1

2018-12-4

0.013%

=25000

=0

2

for now()-A1

if month(A1)==1 && day(A1)==1

>C1+=D1

>D1=0

3


>A1=elapse(A1,1)

>D1+=B1*C1


4

=C1+D1




在 D1 存储了当前的利息,初始值为 0,每循环一天,就增加一天的利息。如果当前循环日期是 1 月 1 日,则把当时的利息加到本金上,再清零利息。等循环结束时,再把当时的本金和利息都加上就可以了。

需要注意的是 B3 格,elapse(d,n) 函数当返回某个日期 d 后 n 天的日期,有了这个动作,循环就可以正常运转了。

elapse 和 interval 类似,也有很多选项来控制精度。而且,在以天为精度时,可以直接用加法表示。即 B3 格可以简单写成 >A1=A1+1,甚至 >A1+=1。

百分数常数可以直接在格中用 % 表示,但表达式中不可以,% 会被认为是取余数的运算符。

【程序设计】 前言及目录

【程序设计】7.2 [字与时] 拆分合并

【程序设计】8.1 [表一表] 结构化数据