[Erlang 0059] Erlang日期与时间处理

  在开发过程中,有两个概念是和地区区域相关的:字符编码和时间;编码和时间的规范演变过程中有文化的冲突有历史的遗留,是软件开发中充满人文气息的一角;关于字符编码我之前整理过一篇文章, [Erlang 0024]Erlang二进制数据处理 这部分知识很有意思,特别是格列佛游记所引出的大端小端概念,妙趣横生;平时笔记中也零零散散记录了一些和时间处理相关的内容,今天按图索骥把相关的资料整理汇集于此.

  
  首先把时间相关的概念解释一下:
 

有关时间的概念

GMT时间
    格林尼治标准时间(Greenwich Mean Time ,GMT)是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线.自1924年2月5日开始,格林尼治天文台每隔一小时会向全世界发放调时信息.
    理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时的时间.由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能与实际的太阳时有误差,最大误差达16分钟.由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治时间已经不再被作为标准时间使用.现在的标准时间,是由原子钟报时的协调世界时(UTC).
 
UTC时间 
  UTC时间是 Universal Time, Coordinated的缩写;中文名为协调世界时,又称世界标准时间或世界协调时间;这个略显怪异的名字来由是这样的:国际电信联盟希望协调世界时能够在所有语言有单一的缩写.英语和法语区的人同时希望各自的语言缩写,CUT和TUC,能够成为国际标准.结果最后妥协使用UTC.
  这套时间系统被应用于许多互联网和万维网的标准中,例如,网络时间协议就是协调世界时在互联网中使用的一种方式.在军事和航空中,协调世界时区会使用“Z”来表示.
   在中国大陆,港澳台的本地时间比UTC快8小时,就会写作UTC+8,俗称 东8区。如果是在本地时间比UTC时间慢的地区,例如夏威夷的时间是比UTC时间慢10小时,就会写作UTC-10,并俗称西10区。
 

[Erlang 0059] Erlang日期与时间处理_第1张图片

 
时区
    理论时区以被15整除的子午线为中心,向东西两侧延伸7.5度,即每15°划分一个时区,这是理论时区.理论时区的时间采用其中央经线(或标准经线)的地方时.所以每差一个时区,区时相差一个小时,相差多少个时区,就相差多少个小时.东边的时区比西边的时区时间来得早.为了避免日期的紊乱,提出国际日期变更线的概念。为了避开国界线,有的时区的形状并不规则,而且比较大的国家以国家内部行政分界线为时区界线,这是实际时区,即法定时区.
所有的时区都相对于UTC时设定.
 
UNIX时间戳
  UNIX时间或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从UTC时1970年1月1日0时0分0秒起至现在的总秒数.
    
   2038年问题:现时大部分使用UNIX的系统都是32位元的,即它们会以32位二进制数字表示时间.但是它们最多只能表示至协调世界时间2038年1月19日3时14分07秒(二进制:01111111 11111111 11111111 11111111),在下一秒二进制数字会是10000000 00000000 00000000 00000000,这是负数,因此各系统会把时间误解作1901年12月13日20时45分52秒(亦有说回归到1970年)。这时可能会令软件发生问题,导致系统瘫痪。
目前解决方案是把系统由32位元转为64位元系统。在64位系统下,此时间最多可以表示到292,277,026,596年12月4日15时30分08秒。
  是不是想到了很久以前的千年虫问题?
 
公历 Gregorian calendar
    公历是现在国际通用的历法,又称格列历,通称阳历.“阳历”又名“太阳历”,系以地球绕行太阳一周为一年,为西方各国所通用,故又名“西历”.公历定义了月,闰月等计算方式;能够废掉帝号纪年法,是文化上的一大进步.除了帝号纪年我国还有干支纪年法,这个很有意思,即把十天干(甲,乙,丙,丁,戊,己,庚,辛,壬,癸)和十二地支(子,丑,寅,卯,辰,巳,午,未,申,酉,戌,亥)分别组合起来,共配成六十组,用来表示年月日的次序,周而复始,循环使用.
    2012年是壬辰年,呵呵,明白为什么好多人选择在今年生孩子了吧?
 
夏令时 DST

[Erlang 0059] Erlang日期与时间处理_第2张图片

    电影《国家宝藏》里面有一个情节就是三个人看到美元背后的4:10以为来不及了,但是意识到夏时令之后,发现还有一个小时的时间,一切还来得及;那么什么是夏令时呢?
    夏时制(Daylight Saving Time:DST)又称“日光节约时制”和“夏令时间”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”.一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电.各个采纳夏时制的国家具体规定不同.目前全世界有近110个国家每年要实行夏令时. 自2011年3月27日开始俄罗斯永久使用夏令时,把时间拨快一小时,不再调回.
 
   据称最早有夏令时构思的是本杰明·富兰克林  夏令时的提出者本杰明·富兰克林(被印在100美元钞票上的人物),他在任美国驻法国大使期间,由于习惯于当时美国农村贵族的早睡早起生活,早上散步时看到法国人10点才起床,夜生活过到深夜.于是他在1784年给《巴黎杂志》的编辑写了一封信,信上说法国人的生活习惯浪费了大好的阳光,建议他们早睡早起,说每年可以节约6千4百万磅蜡烛.但他当时并没有建议实行夏令时,只是建议人们应该早睡早起.直到1907年,一位英国建筑师威廉·维莱特(William Willett)才正式向英国议会提出夏时制的构思,主要是为了节省能源和提供更多的时间用来训练士兵,但议会经过辩论没有采纳.由于名声不及本杰明·富兰克林,所以人们很多都将本杰明·富兰克林当为夏时制的发明者而忽略了威廉·维莱特.
   中国曾在1986-1991年推行夏时制,后停止原因是:1.区域辽阔大部分地区都没有必要实行2.中国大多数的平民都已习惯北京时间,实行夏令时对于他们来说难以接受,认为多此一举.3.夏时制使铁路和航班需要每年修改时间表,造成麻烦和不便.
 
公元纪元
   "公历纪元"是国际通行的纪年体系.以传说中耶稣基督的生年为公历元年,产生于基督教盛行的六世纪,当时,为了扩大教会的统治势力,神父们把任何事情都附会在基督教上.  
 
日期时间格式化表示
    文化差异下日期的表示方式也各有不同,就英语而言英联邦国家和美国就在日期表示惯用法上有不同;要想流畅的沟通就要有日期时间的表示规范,ISO 8601对日期时间表示做了详细的描述,可以看一下维基百科的简介.
 

Erlang Calendar

 
了解了这些之后,我们回归到技术之上,看一下Erlang中有关时间的计算,这基本上集中在 calendar模块. 平时我们调用的erlang:now()得到的是从 00:00 GMT, January 1, 1970起经过的时间,结果构成为:{MegaSecs, Secs, MicroSecs},对于公历计时的起点Erlang回溯到0年.若计算时间差,需转成世界时间之后再进行计算;时间运算基于下面的规则:
 

     erlang:now() Returns the tuple {MegaSecs, Secs, MicroSecs} which is the elapsed time since 00:00 GMT, January 1, 1970 (zero hour) on the assumption that the underlying OS supports this. Otherwise, some other point in time is chosen. It is also guaranteed that subsequent calls to this BIF returns continuously increasing values. Hence, the return value from now() can be used to generate unique time-stamps, and if it is called in a tight loop on a fast machine the time of the node can become skewed.

    It can only be used to check the local time of day if the time-zone info of the underlying operating system is properly configured.

 

• there are 86400 seconds in a day
• there are 365 days in an ordinary year
• there are 366 days in a leap year
• there are 1461 days in a 4 year period
• there are 36524 days in a 100 year period
• there are 146097 days in a 400 year period
• there are 719528 days between Jan 1, 0 and Jan 1, 1970.
以及闰年规则:
• Y is divisible by 4, but not by 100; or
• Y is divisible by 400.
 
calendar模块已经废弃或者不建议使用的模块:
local_time_to_universal_time({Date1, Time1}) -> {Date2, Time2}
time_difference(T1, T2) -> {Days, Time}
 

常用代码

erlang:now转本地时间
15> calendar:now_to_local_time(erlang:now()).
{{2012,5,17},{14,32,6}}
 
erlang:now转世界时间
16> calendar:now_to_universal_time(erlang:now()).
{{2012,5,17},{6,33,2}}
17>
  
验证合法的日期
20> calendar:valid_date({0,0,0}).
false
21> calendar:valid_date({0,1,0}).
false
22> calendar:valid_date({0,1,1}).
true
26> calendar:valid_date(90,1,10). 
true 
  
获得本地时间 年 月 日小时 分 秒
1>  {{Year,Month,Day},{Hour,Min,Second}}=calendar:local_time().
{{2012,5,17},{14,13,14}}
2> b().
Day = 17
Hour = 14
Min = 13
Month = 5
Second = 14
Year = 2012
ok
 
获得世界时间
3> calendar:universal_time().
{{2012,5,17},{6,16,27}}
 
本地时间转换到世界时间
10> calendar:local_time_to_universal_time_dst({{2012,3,12},{3,23,12}}).
[{{2012,3,11},{19,23,12}}]
11> calendar:local_time_to_universal_time_dst({{2012,3,12},{4,23,12}}).
[{{2012,3,11},{20,23,12}}]
12> calendar:local_time_to_universal_time_dst({{2012,3,12},{5,23,12}}).
[{{2012,3,11},{21,23,12}}]
13>  calendar:local_time_to_universal_time_dst({{2012,3,12},{15,23,12}}).
[{{2012,3,12},{7,23,12}}]
14>  calendar:local_time_to_universal_time_dst({{2012,3,12},{16,23,12}}).
[{{2012,3,12},{8,23,12}}]
 
世界时间转本地时间
12> calendar:universal_time_to_local_time({{2012,3,12},{3,23,12}}).
{{2012,3,12},{11,23,12}}
13> calendar:universal_time_to_local_time({{2012,3,12},{5,23,12}}).
{{2012,3,12},{13,23,12}}
14> calendar:universal_time_to_local_time({{2012,3,12},{20,23,12}}).
{{2012,3,13},{4,23,12}}
 
计算是星期几
13> calendar:day_of_the_week(1984,5,15).
2
14> calendar:day_of_the_week(2012,5,17).
4
 
计算是否闰年
5> calendar:is_leap_year(1990).
false
6> calendar:is_leap_year(2000).
true
 
计算某年某月有多少天
7> calendar:last_day_of_the_month(2000,2).
29
8> calendar:last_day_of_the_month(2000,3).
31
9> calendar:last_day_of_the_month(1990,2).
28
 
秒转时间
4> calendar:now_to_datetime(erlang:now()).     
{{2012,5,17},{5,41,24}}
5> calendar:seconds_to_daystime(87400).  
{1,{0,16,40}}
6> calendar:seconds_to_daystime(97400).
{1,{3,3,20}}
7> calendar:seconds_to_daystime(80400).
{0,{22,20,0}}
如果没有超过一天可以使用
8> calendar:seconds_to_time(80400).    
{22,20,0}
9> calendar:seconds_to_time(86400).   
** exception error: no function clause matching calendar:seconds_to_time(86400) (calendar.erl, line 357)
 
 
时间转到秒
10> calendar:time_to_seconds({22,20,0}).
80400
11> calendar:time_to_seconds({1,{22,20,0}}).
** exception error: no function clause matching calendar:time_to_seconds({1,{22,20,0}}) (calendar.erl, line 390)
  
计算Unix Timestamp
timestamp() ->
    calendar:datetime_to_gregorian_seconds(erlang:universaltime()).
或者:
timestamp() ->
    {M, S, _} = erlang:now(),  
    M * 1000000 + S.
 
 
日期时间格式化
16> test:now_to_local_string(erlang:now()).
"2012-05-17T16:14:42.195510+08:00"
17> test:now_to_utc_string(erlang:now()). 
"2012-05-17T08:15:26.907466Z"
18> test:timestamp_to_iso(calendar:local_time()).
"20120517T16:27:18"
下面的代码实现上面的时间格式化效果:
timestamp_to_iso({{Year, Month, Day}, {Hour, Minute, Second}}) ->
    lists:flatten(
      io_lib:format("~4..0w~2..0w~2..0wT~2..0w:~2..0w:~2..0w",
            [Year, Month, Day, Hour, Minute, Second])).


now_to_utc_string({MegaSecs, Secs, MicroSecs}) ->
    {{Year, Month, Day}, {Hour, Minute, Second}} =
    calendar:now_to_universal_time({MegaSecs, Secs, MicroSecs}),
    lists:flatten(
      io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0w.~6..0wZ",
            [Year, Month, Day, Hour, Minute, Second, MicroSecs])).

now_to_local_string({MegaSecs, Secs, MicroSecs}) ->
    LocalTime = calendar:now_to_local_time({MegaSecs, Secs, MicroSecs}),
    UTCTime = calendar:now_to_universal_time({MegaSecs, Secs, MicroSecs}),
    Seconds = calendar:datetime_to_gregorian_seconds(LocalTime) -
            calendar:datetime_to_gregorian_seconds(UTCTime),
    {{H, M, _}, Sign} = if
                Seconds < 0 ->
                {calendar:seconds_to_time(-Seconds), "-"};
                true ->
                {calendar:seconds_to_time(Seconds), "+"}
    end,
    {{Year, Month, Day}, {Hour, Minute, Second}} = LocalTime,
    lists:flatten(
      io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0w.~6..0w~s~2..0w:~2..0w",
            [Year, Month, Day, Hour, Minute, Second, MicroSecs, Sign, H, M])).


% yyyy-mm-ddThh:mm:ss[.sss]{Z|{+|-}hh:mm} -> {MegaSecs, Secs, MicroSecs}
datetime_string_to_timestamp(TimeStr) ->
    case catch parse_datetime(TimeStr) of
    {'EXIT', _Err} ->
        undefined;
    TimeStamp ->
        TimeStamp
    end.

parse_datetime(TimeStr) ->
    [Date, Time] = string:tokens(TimeStr, "T"),
    D = parse_date(Date),
    {T, MS, TZH, TZM} = parse_time(Time),
    S = calendar:datetime_to_gregorian_seconds({D, T}),
    S1 = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
    Seconds = (S - S1) - TZH * 60 * 60 - TZM * 60,
    {Seconds div 1000000, Seconds rem 1000000, MS}.

% yyyy-mm-dd
parse_date(Date) ->
    [Y, M, D] = string:tokens(Date, "-"),
    Date1 = {list_to_integer(Y), list_to_integer(M), list_to_integer(D)},
    case calendar:valid_date(Date1) of
    true ->
        Date1;
    _ ->
        false
    end.

% hh:mm:ss[.sss]TZD
parse_time(Time) ->
    case string:str(Time, "Z") of
    0 ->
        parse_time_with_timezone(Time);
    _ ->
        [T | _] = string:tokens(Time, "Z"),
        {TT, MS} = parse_time1(T),
        {TT, MS, 0, 0}
    end.

parse_time_with_timezone(Time) ->
    case string:str(Time, "+") of
    0 ->
        case string:str(Time, "-") of
        0 ->
            false;
        _ ->
            parse_time_with_timezone(Time, "-")
        end;
    _ ->
        parse_time_with_timezone(Time, "+")
    end.

parse_time_with_timezone(Time, Delim) ->
    [T, TZ] = string:tokens(Time, Delim),
    {TZH, TZM} = parse_timezone(TZ),
    {TT, MS} = parse_time1(T),
    case Delim of
    "-" ->
        {TT, MS, -TZH, -TZM};
    "+" ->
        {TT, MS, TZH, TZM}
    end.

parse_timezone(TZ) ->
    [H, M] = string:tokens(TZ, ":"),
    {[H1, M1], true} = check_list([{H, 12}, {M, 60}]),
    {H1, M1}.

parse_time1(Time) ->
    [HMS | T] =  string:tokens(Time, "."),
    MS = case T of
         [] ->
         0;
         [Val] ->
         list_to_integer(string:left(Val, 6, $0))
     end,
    [H, M, S] = string:tokens(HMS, ":"),
    {[H1, M1, S1], true} = check_list([{H, 24}, {M, 60}, {S, 60}]),
    {{H1, M1, S1}, MS}.

check_list(List) ->
    lists:mapfoldl(
      fun({L, N}, B)->
      V = list_to_integer(L),
      if
          (V >= 0) and (V =< N) ->
          {V, B};
          true ->
          {false, false}
      end
      end, true, List).

 

构造日期字符串


% a function to format date/time properly (e.g. 09 instead of 9)
return_2columns(X) ->
    case length(X) of
        1 ->
            "0" ++ X;
        _ ->
            X
    end.

%%% 显然这里可以直接使用 io_lib:format("~2..0B", [X])

% returns date/time as a properly formatted string (e.g. "01-01-2000 12:12:12")
get_current_time() ->
    {{Y, M, D}, {H, Mi, S}} = calendar:local_time(),
    L = lists:map(fun(X) -> 
                          X2=integer_to_list(X), 
                          return_2columns(X2) 
                  end, 
                  [Y, M, D, H, Mi, S]
                 ),
    [Y2, M2, D2, H2, Mi2, S2] = L,
    Y2 ++ "-" ++ M2 ++ "-" ++ D2 ++ " " ++ H2 ++ ":" ++ Mi2 ++ ":" ++ S2.

 当然下面的代码段也很有可能被用到:

 
day(1) -> "Mon";
day(2) -> "Tue";
day(3) -> "Wed";
day(4) -> "Thu";
day(5) -> "Fri";
day(6) -> "Sat";
day(7) -> "Sun".

month_to_list(1)  -> "Jan";
month_to_list(2)  -> "Feb";
month_to_list(3)  -> "Mar";
month_to_list(4)  -> "Apr";
month_to_list(5)  -> "May";
month_to_list(6)  -> "Jun";
month_to_list(7)  -> "Jul";
month_to_list(8)  -> "Aug";
month_to_list(9)  -> "Sep";
month_to_list(10) -> "Oct";
month_to_list(11) -> "Nov";
month_to_list(12) -> "Dec".

list_to_month("Jan") -> 1;
list_to_month("Feb") -> 2;
list_to_month("Mar") -> 3;
list_to_month("Apr") -> 4;
list_to_month("May") -> 5;
list_to_month("Jun") -> 6;
list_to_month("Jul") -> 7;
list_to_month("Aug") -> 8;
list_to_month("Sep") -> 9;
list_to_month("Oct") -> 10;
list_to_month("Nov") -> 11;
list_to_month("Dec") -> 12.
 

Note:(Day)这个概念是和时区有关的,使用Unix时间戳计算的时候要考虑到时区差异,比如两个Unix时间戳是否在同一天,一天的起始时间的时间戳,等等;软件多语言版本的时候这个问题特别要关注 

 

 晚 安 ! 

 
相关资料:
[1] 时区 http://zh.wikipedia.org/wiki/%E6%97%B6%E5%8C%BA
[2] 国家授时中心网络服务器时间发布  http://www.time.ac.cn/stime.asp
[3] GMT时间  http://zh.wikipedia.org/wiki/GMT
[4] UTC时间  http://zh.wikipedia.org/wiki/%E5%8D%94%E8%AA%BF%E4%B8%96%E7%95%8C%E6%99%82
[5] UNIX时间  http://zh.wikipedia.org/wiki/UNIX%E6%97%B6%E9%97%B4
[6] 千年虫问题  http://zh.wikipedia.org/wiki/%E5%8D%83%E5%B9%B4%E8%99%AB
[7] 公历  http://en.wikipedia.org/wiki/Gregorian_calendar
[8] 夏时制  http://zh.wikipedia.org/wiki/%E5%A4%8F%E6%97%B6%E5%88%B6
[9] 日期格式化  http://en.wikipedia.org/wiki/ISO_8601
[10] $100  http://zh.wikipedia.org/wiki/100%E7%BE%8E%E5%85%83%E7%BA%B8%E5%B8%81
 

你可能感兴趣的:([Erlang 0059] Erlang日期与时间处理)