Perl 6 By Example: Datetime Conversion for the Command Line
我偶爾會在數(shù)據(jù)庫中存儲 UNIX 時間戳, 即從 1970-01-01 開始的秒數(shù)橘忱。我在按照日期查詢數(shù)據(jù)庫中的數(shù)據(jù)時, 需要將 UNIX 時間戳轉換為人類可讀的時間, 所以我寫了個很小的工具來幫助我在 UNIX 時間戳和日期/時間之間來回轉換:
$ autotime 2015-12-24
1450915200
$ autotime 2015-12-24 11:23:00
1450956180
$ autotime 1450915200
2015-12-24
$ autotime 1450956180
2015-12-24 11:23:00
使用庫
Perl 6 的 DateTime 和 Date 模塊會做實際的轉換。
DateTime.new
構造函數(shù)有一個接收單個整數(shù)作為 UNIX 時間戳的變體:
$ perl6 -e "say DateTime.new(1480915200)"
2016-12-05T05:20:00Z
看起來我們已經(jīng)完成了一個方向的轉換,對嗎?
#!/usr/bin/env perl6
sub MAIN (Int $timestamp) {
say DateTime.new($timestamp)
}
我們來運行它:
$ autotime 1450915200
Invalid DateTime string '1450915200'; use an ISO 8601 timestamp (yyyy-mm-ddThh:mm:ssZ or yyyy-mm-ddThh:mm:ss+01:00) instead
in sub MAIN at autotime line 2
in block <unit> at autotime line 2
發(fā)生了什么募强?看起來 DateTime
構造函數(shù)把參數(shù)當作了字符串, 盡管 sub MAIN
的參數(shù)被聲明為 Int
。怎么會變成那樣呢? 我們添加一些調(diào)試輸出:
#!/usr/bin/env perl6
sub MAIN(Int $timestamp) {
say $timestamp.^name;
say DateTime.new($timestamp)
}
打印出:
IntStr
$thing.^name
是 $thing 所屬類的名字。 IntStr 是 Int
和 Str
類的子類, 這就是為什么 DateTime
構造函數(shù)正常地認為 $timestamp 是一個 Str
的原因挟阻。
長話短說, 我們可以在參數(shù)前添加一個 +
前綴使參數(shù)強制為 "真" 整數(shù), 這也是將字符串轉為數(shù)值的通用機制:
#!/usr/bin/env perl6
sub MAIN(Int $timestamp) {
say DateTime.new(+$timestamp)
}
這一次它真的工作了:
$ ./autotime-01.p6 1450915200
2015-12-24T00:00:00Z
輸出是 ISO 8601 樣式的時間戳格式, 對眼睛不太友好伐债。對于小時,分鐘和秒數(shù)都為 0 的日期, 我們真正想要的只有日期:
#!/usr/bin/env perl6
sub MAIN(Int $timestamp) {
my $dt = DateTime.new(+$timestamp);
if $dt.hour == 0 && $dt.minute == 0 && $dt.second == 0 {
say $dt.Date;
}
else {
say $dt;
}
}
這樣看起來更好一點:
$ ./autotime 1450915200
2015-12-24
但是上面那種三個比較都為 0 的寫法實在太丑了, 如果是 4 個, 5 個, 6 個... 那就是又丑又長趣兄。Perl 6 有一個 all
Junction:
if all($dt.hour, $dt.minute, $dt.second) == 0 {
say $dt.Date;
}
all(...)
創(chuàng)建了一個 Junction, 它是幾個其他值的組合值, 它也存儲了一個邏輯模式驻民。當你比較一個 junction 和其他值的時候, 那個比較會自動地應用到該 junction 中的所有值上腺怯。if
語句在布爾上下文中對該 junction 進行求值, 在這個例子中, 當所有的比較為 True
時, if 也返回 True
。
其他類型的 junction 還有 any
, all
, none
川无。考慮到在布爾上下文中, 0 是唯一一個求值為 false 的整數(shù), 我們甚至可以把上面的例子寫為:
if none($dt.hour, $dt.minute, $dt.second) {
say $dt.Date;
}
但是也可能沒有必要搞得那么復雜, 如果 $dt
這個 Datetime 對象轉換為 Date
然后再轉換為 DateTime 而不丟失信息, 那么它肯定是一個 Date:
if $dt.Date.DateTime == $dt {
say $dt.Date;
}
else {
say $dt;
}
DateTime 格式化
如果時間戳沒有被解析為整天, 那么當前我們的腳本的輸出就會像這樣:
2015-12-24T00:00:01Z
其中的 "Z" 表示 UTC 或 "Zulu" 時區(qū)虑乖。
DateTime
類支持自定義格式化, 所以我們來寫一個:
sub MAIN(Int $timestamp) {
my $dt = DateTime.new(+$timestamp, formatter => sub ($o) {
sprintf '%04d-%02d-%02d %02d:%02d:%02d',
$o.year, $o.month, $o.day,
$o.hour, $o.minute, $o.second,
});
if $dt.Date.DateTime == $dt {
say $dt.Date;
}
else {
say $dt.Str;
}
}
現(xiàn)在輸出看起來更好看了:
./autotime 1450915201
2015-12-24 00:00:01
語法 formatter => ...
在參數(shù)上下文中表示具名參數(shù)懦趋。
這樣的代碼我不喜歡, 因為在 DateTime.new
調(diào)用中它是內(nèi)聯(lián)的, 這并不清晰。
我們來單獨寫一個例程:
#!/usr/bin/env perl6
sub MAIN(Int $timestamp) {
sub formatter($o) {
sprintf '%04d-%02d-%02d %02d:%02d:%02d',
$o.year, $o.month, $o.day,
$o.hour, $o.minute, $o.second,
}
my $dt = DateTime.new(+$timestamp, formatter => &formatter);
if $dt.Date.DateTime == $dt {
say $dt.Date;
}
else {
say $dt.Str;
}
}
是的, 你可以把一個子例程聲明放在另一個子例程聲明的正文中; 子例程只是一個普通的詞法符號,就像一個用 my
聲明的變量疹味。
在行 my $dt = DateTime.new(+$timestamp,formatter => &formatter);
中, 語法 &formatter
引用子例程作為一個對象,而不調(diào)用它仅叫。
這是 Perl 6, formatter => &formatter
有一個簡寫: &formatter
。
作為一般規(guī)則,如果要填充一個名稱為變量名稱并且其值為變量值的命名參數(shù), 可以通過寫入 :$variable
創(chuàng)建它糙捺。 作為擴展, :thing
是 thing => True
的縮寫诫咱。
尋找其他途徑
現(xiàn)在, 從時間戳到日期和時間的轉換工作的很好, 讓我們看另一種途徑。
我們的小工具需要解析輸入, 并決定輸入的是時間戳還是日期和可選的時間洪灯。
一種無聊的方式是使用條件:
sub MAIN($input) {
if $input ~~ / ^ \d+ $ / {
# convert from timestamp to date/datetime
}
else {
# convert from date to timestamp
}
}
但我討厭無聊, 所以我想看看一個更令人興奮的(端可擴展)方法坎缭。
Perl 6 支持多重分派。這意味著您可以有多個具有相同名稱但不同簽名的子例程签钩。
Perl 6 自動決定要調(diào)用哪一個掏呼。 您必須通過編寫 multi sub
而不是 sub
來顯式地啟用此功能, 以便 Perl 6 可以捕獲意外的重新聲明。
讓我們看看它在實際中的運用:
#!/usr/bin/env perl6
multi sub MAIN(Int $timestamp) {
sub formatter($o) {
sprintf '%04d-%02d-%02d %02d:%02d:%02d',
$o.year, $o.month, $o.day,
$o.hour, $o.minute, $o.second,
}
my $dt = DateTime.new(+$timestamp, :&formatter);
if $dt.Date.DateTime == $dt {
say $dt.Date;
}
else {
say $dt.Str;
}
}
multi sub MAIN(Str $date) {
say Date.new($date).DateTime.posix
}
我們看一下效果:
$ ./autotime 2015-12-24
1450915200
$ ./autotime 1450915200
Ambiguous call to 'MAIN'; these signatures all match:
:(Int $timestamp)
:(Str $date)
in block <unit> at ./autotime line 17
不是我所想象的铅檩。問題又是整數(shù)參數(shù)自動被轉換為了 IntStr
, Int 和 Str multi
(或候選)都接受它作為參數(shù)憎夷。
避免這種錯誤的最簡單的方法是縮小 Str 候選者接受的字符串的種類。
經(jīng)典的方法是用一個正則表達式粗略驗證傳入的參數(shù):
multi sub MAIN(Str $date where /^ \d+ \- \d+ \- \d+ $ /) {
say Date.new($date).DateTime.posix
}
它確實能工作, 但為什么重復 Date.new 已經(jīng)有用于驗證日期字符串的邏輯昧旨?
如果你傳遞一個看起來不像日期的字符串參數(shù),你會得到這樣的錯誤:
Invalid Date string 'foobar'; use yyyy-mm-dd instead
我們可以使用這種行為約束 MAIN multi
候選者的字符串參數(shù):
multi sub MAIN(Str $date where { try Date.new($_) }) {
say Date.new($date).DateTime.posix
}
在這里額外的 try
是因為子類型約束后面的 where
不應該拋出異常, 而只是返回一個假值拾给。
現(xiàn)在它的工作得像預期的一樣:
$ ./autotime 2015-12-24;
1450915200
$ ./autotime 1450915200
2015-12-24
處理時間
剩下要實現(xiàn)的功能是把日期和時間轉換為時間戳。換句話說, 我們想這樣調(diào)用 autotime 2015-12-24 11:23:00
:
multi sub MAIN(Str $date where { try Date.new($_) }, Str $time?) {
my $d = Date.new($date);
if $time {
my ( $hour, $minute, $second ) = $time.split(':');
say DateTime.new(date => $d, :$hour, :$minute, :$second).posix;
}
else {
say $d.DateTime.posix;
}
}
憑借尾部的?, 新的第二個參數(shù)是可選的 兔沃。 如果存在第二個參數(shù), 我們用冒號將時間字符串分割成小時,分鐘和秒蒋得。 我寫的第一個本能是使用較短的變量名稱, my($h, $m, $s) = $time.split(':')
, 但然后調(diào)用 DateTime
構造函數(shù)看起來像這樣:
DateTime.new(date => $d, hour => $h, minute => $m, second => $s);
所以構造函數(shù)的命名參數(shù)使我選擇更多的自解釋變量名。
所以, 這個可以工作:
./autotime 2015-12-24 11:23:00
1450956180
而且我們還可以檢測它的原形:
$ ./autotime 1450956180
2015-12-24 11:23:00
系好你的安全帶
Perl 6 的隱式變量或主題變量:
for 1..3 {
.say
}
產(chǎn)生如下輸出:
[source]
1
2
3
這個例子中沒有顯式的迭代變量, 所以 Perl 隱式地把當前循環(huán)的值綁定給叫做 $_
的變量粘拾。方法調(diào)用 .say
是 $_.say
的縮寫窄锅。由于我們有一個子例程在同一個變量上調(diào)用了 6 個方法, 所以使用 $_
會有很好的可視效果:
sub formatter($_) {
sprintf '%04d-%02d-%02d %02d:%02d:%02d',
.year, .month, .day,
.hour, .minute, .second,
}
如果你不想求助于函數(shù)定義在詞法作用域中設置 $_
, 那么你可以使用 given VALUE BLOCK
結構:
given DateTime.new(+$timestamp, :&formatter) {
if .Date.DateTime == $_ {
say .Date;
}
else {
.say;
}
}
Perl 6 還提供了對 $_
變量的條件語句的快捷方式,可以用作一個通用的switch語句:
given DateTime.new(+$timestamp, :&formatter) {
when .Date.DateTime == $_ { say .Date }
default { .say }
}
如果你有一個只讀的變量或參數(shù), 那么你可以不使用 $
符號, 雖然你可以在聲明時使用反斜線:
multi sub MAIN(Int \timestamp) {
...
given DateTime.new(+timestamp, :&formatter) {
...
}
}
所以現(xiàn)在完整的代碼看起來像這樣:
#!/usr/bin/env perl6
multi sub MAIN(Int \timestamp) {
sub formatter($_) {
sprintf '%04d-%02d-%02d %02d:%02d:%02d',
.year, .month, .day,
.hour, .minute, .second,
}
given DateTime.new(+timestamp, :&formatter) {
when .Date.DateTime == $_ { say .Date }
default { .say }
}
}
multi sub MAIN(Str $date where { try Date.new($_) }, Str $time?) {
my $d = Date.new($date);
if $time {
my ( $hour, $minute, $second ) = $time.split(':');
say DateTime.new(date => $d, :$hour, :$minute, :$second).posix;
}
else {
say $d.DateTime.posix;
}
}
MAIN 魔法
為我們調(diào)用 sub MAIN
的魔法還為我們提供了一個自動化的用法消息, 如果我們用不匹配任何 multi
的參數(shù)調(diào)用 MAIN, 例如調(diào)用時不提供參數(shù):
$ ./autotime
Usage:
./autotime <timestamp>
./autotime <date> [<time>]
我們可以通過在 MAIN subs 之前添加語義注釋來為這些用法行添加簡短描述:
#!/usr/bin/env perl6
#| Convert timestamp to ISO date
multi sub MAIN(Int \timestamp) {
...
}
#| Convert ISO date to timestamp
multi sub MAIN(Str $date where { try Date.new($_) }, Str $time?) {
...
}
現(xiàn)在用法信息變?yōu)榱?
$ ./autotime
Usage:
./autotime <timestamp> -- Convert timestamp to ISO date
./autotime <date> [<time>] -- Convert ISO date to timestamp
總結
我們已經(jīng)看到了一些 Date 和 DateTime 算法, 但令人興奮的部分是 multi dispatch, 命名參數(shù),帶有 where 從句的子類型約束, given/ when 和 隱式 $_ 變量, 以及一些魔法, 當涉及到 MAIN subs 時。
原文請參見 Perl 6 By Example: Datetime Conversion for the Command Line