本帖最后由 neorobin 于 2012-4-9 15:44 编辑
上接 15# 日期序号算法分析
序号求日期的算法分析
设算法函数为 date(i), i 为待求日期的序号, 返回值为 y.m.d, 其中包含 p 个计算年, d1st 为第 p + 1 个计算年的年始日序号.
首先对 p 作估值, 以
(d1st-1) * 250 / 91311
计算初始估值, 在 32位 cmd 下估值溢出下限为 23519, 故而 p ∈ [1, 20000] 估值不会发生溢出错误.
在第 p + 1 个计算年中:
估值误差范围: [(d1st-1) * 250 / 91311 - p, ((d1st-1) + 365) * 250 / 91311 - p]
在 p ∈ [1, 20000] 内计算得出此估值误差的最小值, 最大值为:
[-0.088522, 0.998171] (-0.088522 : 当 p = 19903; 0.998171 : 当 p = 96)
以 trunc 取整再加 1 后, 估值最小值, 最大值为:
[trunc(p-0.088522)+1, trunc(p+0.998171)+1] = [p, p+1]
估值后, 计算序号误差, 若超过原序号, 表明估值计算得 p+1, 将估值减 1,
再重新计算序号误差:- set /a "y=(i-1)*250/91311+1, dd=i-(365*y + y/4 - y/100 + y/400)"
- set /a "y+=dd>>31, dd=i-(365*y + y/4 - y/100 + y/400)"
复制代码 上面的计算中, 最后得到的 dd 即为第 p+1 个计算年中 m.d 日 对 3.1 日 的偏移值, 从这个偏移值计算 m, 采用如下近似线性的拟合函数:
ind(m) = trunc(f(dd)) = trunc((5*dd + 2)/153)
下表呈现了该函数是完美拟合的, s, e 为每一月的始日和终日对 3.1 日的偏移
month | len | ind | s | e | f(s)*1000 | f(e)*1000 | trunc(f(s)) | trunc(f(e)) | Mar | 31 | 0 | 0 | 30 | 13 | 993 | 0 | 0 | Apr | 30 | 1 | 31 | 60 | 1026 | 1973 | 1 | 1 | May | 31 | 2 | 61 | 91 | 2006 | 2986 | 2 | 2 | Jun | 30 | 3 | 92 | 121 | 3019 | 3967 | 3 | 3 | Jul | 31 | 4 | 122 | 152 | 4000 | 4980 | 4 | 4 | Aug | 31 | 5 | 153 | 183 | 5013 | 5993 | 5 | 5 | Sep | 30 | 6 | 184 | 213 | 6026 | 6973 | 6 | 6 | Oct | 31 | 7 | 214 | 244 | 7006 | 7986 | 7 | 7 | Nov | 30 | 8 | 245 | 274 | 8019 | 8967 | 8 | 8 | Dec | 31 | 9 | 275 | 305 | 9000 | 9980 | 9 | 9 | Jan | 31 | 10 | 306 | 336 | 10013 | 10993 | 10 | 10 | Feb | 28 | 11 | 337 | 364 | 11026 | 11908 | 11 | 11 | Feb(leap) | 29 | 11 | 337 | 365 | 11026 | 11941 | 11 | 11 |
上面表格可用代码得到:- @echo off & setlocal enabledelayedexpansion
- echo mon len ind s e f(s) f(e) int(f(s)) int(f(e))
- for %%i in ("Feb 28" "Feb 29") do (
- set /a "i=100, s=1000, e=1000-1"
- for %%a in ("Mar 31" "Apr 30" "May 31" "Jun 30" "Jul 31" "Aug 31"
- "Sep 30" "Oct 31" "Nov 30" "Dec 31" "Jan 31" %%i) do (
- set "l=%%~a"
- set /a "e+=!l:~-2!, fs=(5*(s-1000)+2)*1000/153+100000,fe=(5*(e-1000)+2)*1000/153+100000"
- set /a "ifs=(5*(s-1000)+2)/153+100,ife=(5*(e-1000)+2)/153+100"
- set "lin=!l! !i:~-2! !s:~-3! !e:~-3! !fs:~-5! !fe:~-5! !ifs:~-2! !ife:~-2!"
- set "lin=!lin: 0= !"
- if %%i=="Feb 28" (echo !lin!) else if %%a=="Feb 29" echo !lin!
- set /a "s+=!l:~-2!, i+=1"
- ))
复制代码 综上, 由序号求日期的完全代码:- set /a "y=(i-1)*250/91311+1,dd=i-(365*y+y/4-y/100+y/400)"
- set /a "y+=dd>>31,dd=i-(365*y+y/4-y/100+y/400)"
- set /a "mi=(5*dd+2)/153"
- set /a "m=(mi+2)%%12+1" & rem {0,1,2,..9,10,11} -> {3,4,5,..12,1,2}
- set /a "y-=m-3>>31" & rem 1,2月到了下一年
- set /a "d=1+dd-(153*mi+2)/5"
复制代码 PS:
计算序号的代码在开始可以将 y 加上一个 400 的倍数, 在序号转为日期的最后将 y 再减去同一个 400 的倍数, 这样就可以将年份 y 的可计算范围向负数调整. 上面 [1, 20000] 的范围分作正负数各一半仍足够大.
和 plp626, terse不约而同的都想到了 153, 2, 5 这个组合.
下面是一个算法互逆性测试代码, 但不能对序号生成的日期正确性作验证测试:- @echo off & setlocal enabledelayedexpansion
- REM 7305155=20000.12.31
- for /l %%i in (0 1 7305155) do (
- set /a "i=%%i"
- set /a "y=(i-1)*250/91311+1,dd=i-(365*y+y/4-y/100+y/400)"
- set /a "y+=dd>>31,dd=i-(365*y+y/4-y/100+y/400)"
- set /a "mi=(5*dd+2)/153"
- set /a "m=(mi+2)%%12+1" & rem {0,1,2,..9,10,11} -> {3,4,5,..12,1,2}
- set /a "y-=m-3>>31" & rem 1,2月到了下一年
- set /a "d=1+dd-(153*mi+2)/5"
- set "test=%%i: !y!.!m!.!d!"
-
- set /a "m+=9,m%%=12,y-=m/10,i=365*y+y/4-y/100+y/400+(m*306+5)/10+d-1"
- set "test=!test! !i!"
- if "!i!" neq "%%i" set "test=!test! error"
- echo !test!
- if "!test:error=!" neq "!test!" pause
- )
复制代码
|