haskell 基础题解(39)

日期问题

【题目】有一类这样的问题,形如:
1949年1月21日 到 2011年5月4日, 一共经过了多少天?
1981年6月6日后的1000天是什么日期?
1960年4月1日是星期几?
从建国到 2019年10月1日,有多少个国庆节是星期日?

不管你使用什么语言,请不要调用任何关于日期的现成API,而是自己编码解决上述问题。

实际上,就算允许你调用日期-时间的API,这类问题往往也很棘手。因为历史的原因,人类风俗习惯甚至宗教等原因,时间-日期问题总是被搞得很复杂。什么闰年、闰月、国际日变更、时区、夏令时。。。。等等一堆概念,都反映在API中,让人头大。

不过,如果仅为了完成上面说的任务,大可不必杀鸡用牛刀。自已写个也很容易。
可能只需要一个小窍门:求 a, b 日期差,只需要分别求出 a, b 到 公元1年1月1日的日期差就可以了。而这比直接求 a b 的差要更容易。

上个 haskell 码,求日期差。

b闰年 :: Int -> Bool
b闰年 year | year `mod` 400 == 0 = True
           | year `mod` 100 == 0 = False
           | year `mod` 4   == 0 = True
           | otherwise           = False

--- 求距离公元1年1月1日 过去了多少天
toDays :: (Int, Int, Int) -> Int
toDays (1,1,1) = 0
toDays (y,1,1) = toDays (y-1,1,1) + if b闰年 (y-1) then 366 else 365 
toDays (y,m,1) = toDays (y,1,1) + sum (take (m-1) [31,x,31,30,31,30,31,31,30,31,30,31])
    where x = if b闰年 y then 29 else 28
toDays (y,m,d) = toDays (y,m,1) + (d-1) 

这段码应该不难理解。有了 toDays 函数,求 a, b 的日期差就很容易:
toDays (2011,5,4) - toDays (1949,1,21)

这是一种很通用的思路。多数时间、日期系统都是这样内部表示的。
比如: Excel,实际上它的数据只有两种:字符串或者数值。
当它存储日期时,实际上存的是 距离 1970 年 1 月 1 日 午夜的天数。只不过它用的是浮点数。其小数部分是:该时刻在一天中的位置。

我们把 Excel 的格式设置为日期并不会改变它的真值,只是改变了真值的表达形式。(这个思路对软件设计十分重要。称为模型+视图)

问题虽说解决了,但不是太 haskell, 还是有点像 c 语言那样处理的影子(我们可不是要同C语言决裂,只是为了训练函数式地思考问题)。

现在考虑到这样一个问题: 究竟是什么使得日期问题看起来这么复杂呢?
是每个月的天数不同!而且2月份天数还不稳定!!

但是: 1 年有12个月,这个是不变的!! 年分不应该跟它们一起混,被月给带坏了啊。

我们计划用 haskell 构造一个 从公元1年1月1起,每个月有多少天的无限列表。只有月,没有年!无限列表!! 说干就干:

--从公元1年1月开始,逐月的天数无限表
monthDays :: [Int]
monthDays = concat [ [31,f x,31,30,31,30,31,31,30,31,30,31] | x<-[1..] ]
    where f x = if b闰年 x then 29 else 28

--- 求距离公元1年1月1日 过去了多少天
toDays' :: (Int, Int, Int) -> Int
toDays' (y,m,d) = let passMon = (y-1) * 12 + (m-1)
    in sum (take passMon monthDays) + (d - 1)

---上面函数的逆函数,距离公元1年1月1日的天数 转为 日期
toDate :: Int -> (Int, Int , Int)
toDate n = 
    let m = f n monthDays  --- 多少个满月
        days = sum $ take m monthDays   ------用掉了多少天
    in (m `div` 12 + 1, m `mod` 12 + 1, n - days + 1)
    where
    f d mon | d < head mon = 0
            | otherwise    = 1 + f (d - head mon) (tail mon)

---某个日期过了 n 天后的日期
dateAdd :: (Int, Int, Int) -> Int -> (Int, Int, Int)
dateAdd (y,m,d) n = toDate $ toDays (y,m,d) + n

这个新版本的 toDays 用了月份天数的无限列表,舒服多了。
而且,toDate 这个逆问题的解决也容易很多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值