本帖最后由 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)"COPY |
上面的计算中, 最后得到的 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" | | ))COPY |
综上, 由序号求日期的完全代码: | 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"COPY |
PS:
计算序号的代码在开始可以将 y 加上一个 400 的倍数, 在序号转为日期的最后将 y 再减去同一个 400 的倍数, 这样就可以将年份 y 的可计算范围向负数调整. 上面 [1, 20000] 的范围分作正负数各一半仍足够大.
和 plp626, terse不约而同的都想到了 153, 2, 5 这个组合.
下面是一个算法互逆性测试代码, 但不能对序号生成的日期正确性作验证测试: | @echo off & setlocal enabledelayedexpansion | | | | 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 | | )COPY |
|