Board logo

标题: [日期时间] [已更新]批处理单行版时间日期计算 算法讨论 [打印本页]

作者: plp626    时间: 2012-4-3 22:15     标题: [已更新]批处理单行版时间日期计算 算法讨论

本帖最后由 plp626 于 2012-4-15 11:55 编辑

原来讨论的问题描述较长,为方便大部分人分享讨论结果,把代码放在一楼,原讨论的问题移至2楼;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

下面仅是零星的一些函数;功能比较单一,是为了大家可以根据需求自己定制功能;
核心的两个函数date2i和i2date算法思想参加15楼分析;

生活实用版:1900年3月1日~2100年3月1日(不含)之间的日期推算
  1. :: date2i // 求1900-3-1到y-m-d所经历的天数i; y-m-d 范围:[1900-3-1, 2100-3-1)
  2. set/a "m=(m+9)%%12,y-=m/10+1900,i=365*y+y/4+(m*153+2)/5+d-1"
复制代码
  1. :: i2date // 求1900-3-1日后第i天的日期; i 的范围:[0, 73049)
  2. set/a "y=(4*i+3)/1461,t=i-y*365-y/4,m=(t*5+2)/153+2,d=t-(m*153-304)/5+1,y+=m/12+1900,m=m%%12+1"
复制代码
数值计算版(欢迎大家测试):0-3-1 (序号0)~ 33301-3-1(序号12162940,不含该序号) 之间的日期推算
  1. :date2i <year> <month> <day> <RetVarName>
  2. setlocal&set/a y=%1,m=%2,d=%3
  3. set/a m=(m+9)%%12,y-=m/10,i=365*y+y/4-y/100+y/400+(m*153+2)/5+d-1
  4. endlocal&set %4=%i%&goto:eof
复制代码
  1. :i2date <index> <Ret1> <Ret2> <Ret3>
  2. setlocal&set/a i=%1, y=(4*i+999)/1461
  3. set/a "y+=i-y*365-y/4+y/100-y/400>>9,t=i-y*365-y/4+y/100-y/400"
  4. set/a "m=(t*5+2)/153,d=t-(m*153+2)/5+1,y+=(m+2)/12,m=(m+2)%%12+1"
  5. endlocal&set/a %2=%y%,%3=%m%,%4=%d%&goto:eof
复制代码
-----------------------------定制---------------------------------------
  1. ::  获取指定日期的星期数;返回值为0表示星期日,返回值为1表示星期1,。。。类推
  2. :date2week <year> <month> <day> <RetVarName>
  3. call:i2date %*&set/a %4=(%4+3)%%7&goto:eof
复制代码
  1. @echo off
  2. :: # 获取指定日期之前或之后的日期 nextdate
  3. :: 日期格式(仅支持月数,日数前面 有1位0的格式):
  4. ::           2012-1-01,2012-01-11,2012-1-1,1941-10-01,...
  5. :: 适用范围: 不好描述,完全够日常生活使用;
  6. :: 要计算100年后或100年前的,请选择数值计算版date2i;i2date进行定制;
  7. rem  函数调用格式演示:
  8. call:nextdate mydate="2012-1-1" -7
  9. echo %mydate%
  10. call:nextdate mydate="2012-01-1" -7
  11. echo %mydate%
  12. call:nextdate mydate="2012-1-01" -7
  13. echo %mydate%
  14. call:nextdate mydate="2012-01-01" -7
  15. echo %mydate%
  16. pause&goto:eof
  17. :nextdate <RetVarName> <"DATE"> <[+|-]int> // 获得 给定日期第n天 的日期
  18. setlocal&set tp=%~2
  19. for /f "tokens=1-3delims=-" %%a in ("%tp:-0=-%")do (
  20.   set/a "y=%%a,m=%%b,d=%%c+%3,m=(m+9)%%12,y-=m/10+1900"
  21.   set/a "i=365*y+y/4+(m*153+2)/5+d-1,y=(4*i+3)/1461,t=i-y*365-y/4"
  22.   set/a "m=(t*5+2)/153+2,d=t-(m*153-304)/5+1,y+=m/12+1900,m=m%%12+1"
  23. )
  24. endlocal&set %1=%y%-%m%-%d%&goto:eof
复制代码
  1. :: 求两个日期相隔的天数
  2. :edate <RetVarName> <"DATE1"> <"DATE2">  
  3. setlocal&set d1=%~2&set d2=%~3
  4. :: 参考代码
  5. set d1=%d1:-= %&set d2=%d2:-= %
  6. call :date2i %d1: 0= % r1
  7. call :date2i %d2: 0= % r2
  8. set/a c=r2-r1
  9. endlocal&set ans.%~1=%c%&set ans.%~1&goto:eof
复制代码

作者: plp626    时间: 2012-4-3 22:27

本帖最后由 plp626 于 2012-4-15 09:37 编辑

{~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
本贴主要讨论时间日期计算的算法思想(几乎纯数学,外加set/a运算技巧)
可参照 HAT翻译了Ritchie Lawrence 的时间日期函数库;为引子,开启大家思路;

方便交流,做如下约定:
一个时间变量(可以称作复合型变量)由6个整数组成,分别为:【年数,月数,日数,时数,分数,秒数】
  1. :: 方便讨论这6个整数分别用如下字母表示[y,m,d,h,f,s]
  2. y取值范围[-10000,10000];
  3. (暂时不考虑y为负值的情形,只考虑公元1年1月1日之后的情形;视难度而定)
  4. m取值范围[1,12]
  5. d取值范围[1,31]
  6. h取值范围[0,23]
  7. f取值范围[0,59]
  8. s取值范围[0,59]
  9. :: 比如 2012-04-03,1:02:09秒这个时间对应的值为 y=2012,m=4,d=3,h=1,f=2,s=9
复制代码
本贴重点是算法思想,阐述思路是重点;
代码虽然作为载体不很重要,但也请给出最简短高效的实现代码;
(在便于阅读的条件下,一行set/a 实现最好)

主要解决一下问题;
(一)给定两个时间,求其相隔的天数;或者秒数
Q1:
     y1,m1,d1,h1,f1,s1 (初时间); y1,m1,d1,h2,f2,s2(末时间) // 初末时间符合客观现实
     求其相隔多少秒sec
  1. :fun1
  2. set/a y=2012,m=4,d=3,h1=1,f1=2,s1=9
  3. set/a                h2=1,f2=3,s2=9
  4. rem ---- 你的代码1 -----
  5. echo %out%
  6. rem 输出值out=60
复制代码
Q2:  
     y1,m1,d1,h1,f1,s1 (初时间); y2,m2,d2,h1,f1,s1(末时间) // 初末时间符合客观现实
     求其相隔多少天day
  1. :fun2
  2. set/a y1=2012,m1=4,d1=3,h=1,f=2,s=9
  3. set/a y2=2012,m2=5,d2=3
  4. rem ---- 你的代码1 -----
  5. echo %out%
  6. rem 输出值out=31
复制代码
Q3:
     y1,m1,d1,h1,f1,s1 (初时间); y2,m2,d2,h2,f2,s2(末时间) // 初末时间符合客观现实
     求其相隔多少天day+多少秒sec (day,sec 为 非负数,day范围[0,+2147483647]sec范围为[0,86399])
  1. :fun3
  2. set/a y1=2012,m1=4,d1=3,h1=1,f1=2,s1=9
  3. set/a y2=2012,m2=4,d2=5,h2=1,f2=2,s2=10
  4. rem ---- 你的代码1 -----
  5. echo %out%
  6. rem 输出值out=2 1
复制代码
(二)给定一个时间,y,m,d,h,f,s 求其 x天或x秒之后的时间
Q4:
       给定 y,m,d,h,f,s;求x天后的;y,m,d,h,f,s (x为负数表示之前,x范围:[-2147483647, +2147483648])
  1. :fun4
  2. set/a y=2012,m=4,d=3,h=1,f=2,s=9
  3. set/a x=-4
  4. rem ---- 你的代码 -----
  5. echo %out%
  6. rem 输出值out=2012 3 30 1 2 9
复制代码
Q5:
   给定 y,m,d,h,f,s;求x秒后的;y,m,d,h,f,s (x为负数表示之前,x范围:[-86399, +86399])
  1. :fun5
  2. set/a y=2012,m=4,d=3,h=1,f=2,s=9
  3. set/a x=-61
  4. rem ---- 你的代码 -----
  5. echo %out%
  6. rem 输出值out=2012 4 3 1 1 8
复制代码
(三)给定一个日期,求其星期数
  1. :fun6 给定一个日期,求其星期数
  2. set/a y=2012,m=4,d=3
  3. rem ---- 你的代码 -----
  4. echo %out%
  5. rem 输出值out=2
复制代码
为建立适合cmd的时间变量数据结构,增加一个成员;跑秒,即百分之一秒;

这样一个时间变量有7个成员(年,月,日,时,分,秒,跑秒),为方便交流约定分别用字母(y,m,d,h,f,s,p)表示;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

回复 7# CrLf
Q1就是Q3的特殊情形(同一天内的两时间差)
Q2也是Q3的特殊情形(同一时分秒下,两日期相隔的天数)
对于你说的问题我不够秒数不细致造成误会;sec取值范围为[0,86399](已更新);
把第一类问题分解成Q1,Q2,是有原因的;
一天=86400秒,秒数可以转换为天数,天数也能转换为秒数,那么求时间差,给出day+sec不是多次一举?
非也,set/a 能支持的最大数值2147483647,2147483647sec=24855day=58year
为了能兼容表示超过58年的情形,用两个数表示时间差,day和sec;day相当于时间差值的高位,sec相当于时间差值的低位;
所以,sec的取值范围为[0,86399]
第一类问题的核心是Q2,这里推荐大家参看15楼的思想;
对于第二类问我还没好思路,大家继续参看15楼的思想,交流下;

把算法思想吃透了,代码真就是工具了;
-----------------------------------------------------------

回复 5# CrLf

只测试了你的天数转日期,是把1969-9-11作为参考日期的第0天;只见代码不见思想;
  1. :d2ymd <day> <ret:y> <ret:m> <ret:d>
  2. @echo off
  3. :d2ymd
  4. setlocal enabledelayedexpansion
  5. set/a d=%1
  6. set/a "d+=719050,d-=d/1461-d/36524+d/146097,y=d/365+1,r=^!(y%%4)-^!(y%%100)+^!(y%%400),d+=(d%%=365)/(212+r)*30+r,d+=^!^!(d/(59+r))*(2-r),m=d/61*2+d%%61/31,d-=m*61/2+m*61%%2-1,m+=1-m/8"
  7. endlocal&if %4. neq . (set %2=%y%&set %3=%m%&set %4=%d%)else echo %y% %m% %d%
复制代码
保存为d2ymd.bat,发现不少bug:
  1. ->d2ymd 841
  2. 1971 12 31
  3. ->d2ymd 842
  4. 1972 1 2
  5. ->d2ymd 1223
  6. 1973 1 16
  7. ->d2ymd 1222
  8. 1973 1 16
  9. 还有很多bug。。。
复制代码
---------------------------------------------
作者: plp626    时间: 2012-4-3 23:03

我先来:
Question1:给定两个时间(同一天),求相隔的秒数
这个应该是最简单的;
思路描述:
先看看怎么计算327-179
(3-1)*100+(2-7)*10+(7-9)*1
1小时=3600秒
1分钟=60秒
类比得到h1,f1,s1 到 h2,f2,s2
相隔的秒数=(h2-h1)*3600+(f2-f1)*60+s2-s1
  1. set/a y=2012,m=4,d=3,h1=1,f1=2,s1=9
  2. set/a                h2=1,f2=3,s2=9
  3. rem -------------------
  4. set out=(h2-h1)*3600+(f2-f1)*60+s2-s1
  5. rem -------------------
  6. echo %out%
  7. rem 输出值out=60
复制代码

作者: neorobin    时间: 2012-4-3 23:19

google:
Date calculation algorithm
有些有价值的参考
作者: CrLf    时间: 2012-4-3 23:53

本帖最后由 CrLf 于 2012-4-4 01:17 编辑

原型:
  1. set/a days=d-719446+30*m+m/9*-~m/2+!(m/9)*m/2+!!(m/3)*(!(y%%4)-!(y%%100)+!(y%%400)-2)+y*365+~-y/4-~-y/100+~-y/400
  2. rem 日期转天数
  3. set/a d+=719050,d-=d/1461-d/36524+d/146097,y=d/365+1,r=!(y%%4)-!(y%%100)+!(y%%400),d+=(d%%=365)/(212+r)*30+r,d+=!!(d/(59+r))*(2-r),m=d/61*2+d%%61/31,d-=m*61/2+m*61%%2-1,m+=1-m/8
  4. rem 天数转日期
  5. set /a m+=!(m/=3)*12,week=(d+2*m+3*(m+1)/5+(y-=m/13)+y/4-y/100+y/400+1)%%7
  6. rem 日期转星期
复制代码

作者: ivor    时间: 2012-4-4 00:14

这个很感兴趣,占位子
作者: CrLf    时间: 2012-4-4 00:18

本帖最后由 CrLf 于 2012-4-4 00:41 编辑

Q1:
  1. @echo off
  2. set/a y1=2012,m1=4,d1=3,h1=1,f1=2,s1=9
  3. set/a y2=y1,m2=m1,d2=d1,h2=1,f2=3,s2=9
  4. set/a out=(d2-d1+30*(m2-m1)+m2/9*-~m2/2+!(m2/9)*m2/2+!!(m2/3)*(!(y2%%4)-!(y2%%100)+!(y2%%400)-2)-m1/9*+~m1/2-!(m1/9)*m1/2-!!(m1/3)*(!(y1%%4)-!(y1%%100)+!(y1%%400)-2)+(y2-y1)*365+~-y2/4-~-y2/100+~-y2/400-~-y1/4+~-y1/100-~-y1/400)*86400+(h2-h1)*3600+(f2-f1)*60+s2-s1
  5. echo %out%
  6. rem 输出值out=60
  7. pause
复制代码
思路:
  1. 年份之差*365+天数之差
  2. 按大小月、平闰年修正天数
  3. 所得天数乘一日总秒数
  4. 最后加上秒数差即可
复制代码
其余几个命题同样可依照几个原型公式稍加修改


与 Q1 同类的:
Q2:
  1. @echo off
  2. set/a y1=2012,m1=4,d1=3,h1=1,f1=2,s1=9
  3. set/a y2=y1,m2=5,d2=3,h2=1,f2=2,s2=9
  4. set/a out=d2-d1+30*(m2-m1)+m2/9*-~m2/2+!(m2/9)*m2/2+!!(m2/3)*(!(y2%%4)-!(y2%%100)+!(y2%%400)-2)-m1/9*+~m1/2-!(m1/9)*m1/2-!!(m1/3)*(!(y1%%4)-!(y1%%100)+!(y1%%400)-2)+(y2-y1)*365+~-y2/4-~-y2/100+~-y2/400-~-y1/4+~-y1/100-~-y1/400+((h2-h1)*3600+(f2-f1)*60+s2-s1)/86400
  5. echo %out%
  6. rem 输出值out=31
  7. pause
复制代码
Q3:
  1. @echo off
  2. set/a y1=2012,m1=4,d1=3,h1=1,f1=2,s1=9
  3. set/a y2=2012,m2=4,d2=5,h2=1,f2=2,s2=10
  4. set/a days=d2-d1+30*(m2-m1)+m2/9*-~m2/2+!(m2/9)*m2/2+!!(m2/3)*(!(y2%%4)-!(y2%%100)+!(y2%%400)-2)-m1/9*+~m1/2-!(m1/9)*m1/2-!!(m1/3)*(!(y1%%4)-!(y1%%100)+!(y1%%400)-2)+(y2-y1)*365+~-y2/4-~-y2/100+~-y2/400-~-y1/4+~-y1/100-~-y1/400,secs=(h2-h1)*3600+(f2-f1)*60+s2-s1
  5. set out=%days% %secs%
  6. echo %out%
  7. rem 输出值out=2 1
  8. pause
复制代码
顺便说一下,楼主似乎没有明确说明天数和秒数的概念,天数的计算是否要精确到秒?秒数的取值范围有多大(需要将相隔天数转为对应秒数吗)?
作者: CrLf    时间: 2012-4-4 01:14

Q4:
  1. @echo off
  2. set/a y=2012,m=4,d=3,h=1,f=2,s=9
  3. set/a x=-4
  4. set/a d=d-397+30*m+m/9*-~m/2+!(m/9)*m/2+!!(m/3)*(!(y%%4)-!(y%%100)+!(y%%400)-2)+y*365+~-y/4-~-y/100+~-y/400+x,d-=d/1461-d/36524+d/146097,y=d/365+1,r=!(y%%4)-!(y%%100)+!(y%%400),d+=(d%%=365)/(212+r)*30+r,d+=!!(d/(59+r))*(2-r),m=d/61*2+d%%61/31,d-=m*61/2+m*61%%2-1,m+=1-m/8
  5. set out=%y% %m% %d% %h% %f% %s%
  6. echo %out%
  7. rem 输出值out=2012 4 3 1 1 8
  8. pause
复制代码
思路:
  1. 取天数加上偏移量
  2. 计算闰年数量取得年数
  3. 取得月份数量
  4. 取得天数(好像都是废话...)
复制代码
大同小异的 Q5:
  1. set/a s=h*3600+f*60+s+x,d=d-397+30*m+m/9*-~m/2+!(m/9)*m/2+!!(m/3)*(!(y%%4)-!(y%%100)+!(y%%400)-2)+y*365+~-y/4-~-y/100+~-y/400+s/86400,d-=d/1461-d/36524+d/146097,y=d/365+1,r=!(y%%4)-!(y%%100)+!(y%%400),d+=(d%%=365)/(212+r)*30+r,d+=!!(d/(59+r))*(2-r),m=d/61*2+d%%61/31,d-=m*61/2+m*61%%2-1,m+=1-m/8,h=s/3600%%60,f=s/60%%60,s%%=60
复制代码

作者: CrLf    时间: 2012-4-4 01:17

Q6 照搬函数里的现成公式...
  1. @echo off
  2. set/a y=2012,m=4,d=3
  3. set /a m+=!(m/=3)*12,out=(d+2*m+3*(m+1)/5+(y-=m/13)+y/4-y/100+y/400+1)%%7
  4. echo %out%
  5. rem 输出值out=2
  6. pause
复制代码

作者: find    时间: 2012-4-4 01:18

各种大神
各种葱白
作者: terse    时间: 2012-4-4 01:59

本帖最后由 terse 于 2012-4-4 03:35 编辑

个人较倾向于转换成儒略日 然后计算
  1. @echo off&setlocal enabledelayedexpansion
  2. for /l %%i in (1 1 31) do set "D%%i=0%%i"
  3. set "Xq=一二三四五六日"
  4. set "Ymda=1990 1 1"&set "Ymdb=2012 4 3"
  5. for /f "tokens=1-6" %%i in ("%Ymda% %Ymdb%") do set /a "JDA=%%k-32075+1461*(%%i+4800+(%%j-14)/12)/4+367*(%%j-2-(%%j-14)/12*12)/12-3*((%%i+4900+(%%j-14)/12)/100)/4,JDB=%%n-32075+1461*(%%l+4800+(%%m-14)/12)/4+367*(%%m-2-(%%m-14)/12*12)/12-3*((%%l+4900+(%%m-14)/12)/100)/4,JD=JDB-JDA"
  6.    echo %Ymda% 至 %Ymdb% 相隔  %JD% 天
  7.    REM 查询N天前或后的日期;
  8.    set/p N=请输入天数(往前查询加上-如:-10000 表示10000天前的日期。):
  9.    set /a JD=N+JDA,W=JD%%7,JD+=68569,N=(4*JD)/146097,JD-=(146097*N+3)/4,I=(4000*JD+1)/1461001,JD-=(1461*I)/4-31,J=(80*JD)/2447,D=JD-(2447*J)/80,JD=J/11,M=J+2-(12*JD),Y=100*(N-49)+I+JD
  10.    for /f "tokens=1-3" %%i in ("!M! !D! !W!") do echo;你查询的是:!Y!年!D%%i:~-2!月!D%%j:~-2!日 星期!Xq:~%%k,1!
  11. pause
复制代码

作者: CrLf    时间: 2012-4-4 11:56

本帖最后由 CrLf 于 2012-4-4 12:01 编辑

根据儒略日公式化简所得的 Q1 方案:
  1. @echo off&setlocal enabledelayedexpansion
  2. set/a y1=2012,m1=4,d1=3,h1=1,f1=2,s1=9
  3. set/a y2=y1,m2=m1,d2=d1,h2=1,f2=3,s2=9
  4. set /a "out=(1461*(y2+(m2-14)/12)/4+367*(m2-2-(m2-14)/12*12)/12-3*((y2+(m2-14)/12)/100+1)/4-1461*(y1+(m1-14)/12)/4-367*(m1-2-(m1-14)/12*12)/12+3*((y1+(m1-14)/12)/100+1)/4-d1+d2)*86400+(h2-h1)*3600+(f2-f1)*60+s2-s1"
  5. echo %out%
  6. rem 输出值out=60
  7. pause
复制代码
确实比早先那个从元年开始硬算的方案简洁
作者: fatcat    时间: 2012-4-4 12:40

关于测试:

对于所有 日期序号 类型的算法: (任何日期对应一个整数, 即序号; 任何一个"明天"的序号=任何一个"今天"的序号+1)
所有正确的算法对两个给定的日期的 差都会是一致的,  
所有正确的算法 得出任何给定的日期的 星期都会是一致的
不同的算法可以取不同的基准日期(可计算范围也可能不同),  用相同的日期参数得到的日期序号可以不同

用 两个日期的差 和 任何日期的星期 在算法间作对比, 可以找出错误的算法, 但不能证明某算法是正确的.

把一个思路非常简单, 非常容易理解的算法 假设 为一个正确的算法.
作者: BillGates    时间: 2012-4-4 23:17

希望有更多的函数,可是这些函数怎么调用啊?
作者: neorobin    时间: 2012-4-6 00:12     标题: 日期序号算法分析

本帖最后由 neorobin 于 2012-4-9 15:47 编辑
  1. set /a "m+=9, m%%=12, y-=m/10, index=365*y + y/4 - y/100 + y/400 + (m*306 + 5)/10 + d - 1
复制代码
计算年: 从历法年 x.3.1 日到 (x+1).2.(28/29) 日, 定义为一个计算年,
一个计算年的长度不固定:
365: 当 x+1 是平年份
366: 当 x+1 是闰年份

一个计算年中, 3.1 日称为年始日, 2.(28/29) 日称为年终日
以 0 年 3 月 1 日为基准日期, 序号设为 0, 其后所有日期的序号依次加 1.
基准日期就是第 1 个计算年的年始日.
前 400 个计算年的始日, 终日, 长度, 始日序号, 终日序号如下:
pfirstlastleni(first)i(last)
10.3.11.2.283650364
21.3.12.2.28365365729
32.3.13.2.283657301094
43.3.14.2.2936610951460
...
87.3.18.2.2936625562921
...
9998.3.199.2.283653579436158
10099.3.1100.2.283653615936523
...
400399.3.1400.2.29366145731146096


从基准日期开始, p 个计算年的总天数公式为(全作平年算, 加上 4 的倍数的闰年的个数, 减去整百年份的个数, 再加上 400 的倍数闰年份的个数):

365*p + p/4 - p/100 + p/400

其中包含的最后的年终日和年始日的序号为:

最后一个年终日的序号 = 365*p + p/4 - p/100 + p/400 - 1

最后一个年始日的序号 = 365*q + q/4 - q/100 + q/400      (其中, q=p-1)

对于历法日期 y.m.d, 首先假定它不是一个年终日,
从基准日期到 小于这个日期的最后一个年终日, 就得到 t 个计算年:

t = y:  当 m >= 3 时; 或者 t = y-1:  当 m < 3 时.

计算 t 的代码:
  1. set /a n=m, n+=9, n%=12, t=y, t-=n/10
复制代码
第 t 个年终日的序号 = 365*t + t/4 - t/100 + t/400 - 1
第 t+1 个年始日的序号 = 365*t + t/4 - t/100 + t/400

上述计算中的
  1. set /a n=m, n+=9, n%=12
复制代码
将 m 线性映射到一个新的变量 n, 映射关系为:
m -> n : {3, 4, 5, ... 12, 1, 2} -> {0, 1, 2, ... 9, 10, 11}

n 不再是历法月份的意义, 而是一个计算年中每一个月份对起始月份 3 月的偏移数.
这个 n 将有助于我们构造一个结构简单的离散函数来计算每个月的起始日对 3.1 日的偏移量.

于是:

y.m.d 的序号 = 第 t+1 个年始日(3.1 日)的序号 + n.1 日对 3.1 日的偏移 + d 日对 1 日的偏移

m.1 日对 3.1 日的偏移 用一个关于 n 的近似线性的离散函数来实现:

int(f(n)) = int((n*306 + 5)/10)

这里 int() 是截尾式取整, 即把参数的小数部分直接去掉, 而不是四舍五入的方式.
Monthnf(n)*10int(f(n))int(f(n+1))-int(f(n))
Mar05031
Apr13113130
May26176131
Jun39239230
Jul.4122912231
Aug5153515331
Sep6184118430
Oct7214721431
Nov8245324530
Dec9275927531
Jan10306530631
Feb113371337

第 n 个计算月的天数: Δint(f(n)) = int(f(n+1))-int(f(n))   (n < 11)
Δint(f(n)) 的平均值 = 30.6 ≈ (31+30+31+30+31+31+30+31+30+31+31)/11

上面表中 f(n)*10, int(f(n)), int(f(n+1))-int(f(n)) 这 3 列可用如下代码在命令行运行得到
  1. for /l %a in (0 1 11) do >nul set /a "fn=%a*306+5" & set fn
  2. for /l %a in (0 1 11) do >nul set /a "fn=%a*306+5, ifn=fn/10" & set ifn
  3. for /l %a in (0 1 10) do >nul set /a "delta=((%a+1)*306+5)/10-(%a*306+5)/10" & set delta
复制代码
以上, 对 年始日序号 和 月始日序号偏移 的计算, 为了方便而引入了变量 t 和 n, 现在我们去掉这两个变量,
给出计算日期 y.m.d 的序号 i 的伪代码:

i(y, m, d) {
  set /a "m+=9, m%=12, y-=int(m/10)"
  return 365*y + y/4 - y/100 + y/400 + int((m*306 + 5)/10) + d - 1
}

仅就此函数而言, 返回值也可不要最后的 "- 1", 只是把任意日期的序号增长了 1, 或者认为将日期序号轴的原点左移了一天.

cmd 的代码在本文开头已给出.

下接 25# 序号求日期的算法分析

参考原文:
http://alcor.concordia.ca/~gpkatch/gdate-algorithm.html
http://alcor.concordia.ca/~gpkatch/gdate-method.html
作者: CrLf    时间: 2012-4-6 00:54

回复 15# neorobin


    叹为观止,m*306 真是绝妙的一招
作者: plp626    时间: 2012-4-7 11:17

本帖最后由 plp626 于 2012-4-7 19:54 编辑

回复 15# neorobin

算法思想体现出了数学之美,深刻,很欣赏;之前用(365.2422±x)常数*year 试着矫正year2day, 对这类整数域上的曲线拟合缺少资料。。。该问题搁置。。。

看了下matlab里面的datenummx.c源代码,year2day用了一个函数,ceil(x),向右取整;
ceil函数在cmd中实现相对繁琐;t=ceil(a/b)  同 set/a "t=a/b+!!(a%b)";
  1. static double cdm[] = {0,31,59,90,120,151,181,212,243,273,304,334};
  2.         if (mon < 1) {
  3.             mon = 1;
  4.         }
  5.         if (mon > 12) {
  6.             y += (mon-1)/12;
  7.             mon = ((mon-1) % 12) + 1;
  8.         }
  9.         *t = 365.*y + ceil(y/4.) - ceil(y/100.) + ceil(y/400.) + cdm[mon-1] + *d;
  10.         if (mon > 2) {
  11.             iy = (int) y;
  12.             if ((iy%4 == 0) && (iy%100 != 0) || (iy%400 == 0)) {
  13.                 *t += 1.;
  14.             }
  15.         }
复制代码
对于月份的转换,没有像儒略日那样,把1,2月份当做上年的末月;而是传统思路,把其当做平年处理,再判断,当所在年份为闰年,且月份大于2时,再+1;

neorobin给出的资料把1,2月份当做上年月份处理,可以将ceil函数转换为fix函数
(fix(a/b)=set/a "a/b",向零取整;15楼的int()取整函数就是向零取整)
也使得月份转换为天数之间建立了向整数近似的线性关系(如果把1,2月份当做当年月份处理,映射关系不是单调的)
从而大大简化了代码;

比较了下,matlab里的datenummx.c的源代码兼容公元前,月份为任意整数,天数为任意整数的情形,但思想在cmd中实现挺繁琐;
[attach]5100[/attach]
作者: plp626    时间: 2012-4-7 12:05

大家 讨论下 儒略日 的思想,为何 “参考日期(第零天)是 1858 年 11 月 17 日”
作者: plp626    时间: 2012-4-7 18:58

本帖最后由 plp626 于 2012-4-7 19:03 编辑

http://alcor.concordia.ca/~gpkatch/gdate-algorithm.html
月份转当年天数:(306*m+5)/10是如何构造的?常数5很像是凑出来的,换成4也可以;
作者: terse    时间: 2012-4-7 23:44

本帖最后由 terse 于 2012-4-7 23:55 编辑

1858 年 11 月 17 日 应该是简化后的吧
儒略日应该推前-4712年1月1日 是儒略日的第0日
这里的 306 是否可看作是30.5833333的近似值  也就是 30.5833333 * M  
这里 (306*m+5)/10 可看作 30.6*M+0.5

这里的 0.5 应该是一个修正 就是+0.5天 因为儒略日简化前 是以中午12点起始计算的

现在计算儒略日公式  譬如(SET /A "M=(M+9)%%12+3,Y=Y-M/13,JDB=365*(4712+Y)+(4712+Y)/4+153*(M+1)/5-Y/100+Y/400+D-61")

只是计算 1582年10月15后的 之前的要+10天  也就是 1582年10月4日至1582年10月14日是空白的
作者: neorobin    时间: 2012-4-9 01:50

本帖最后由 neorobin 于 2012-4-9 01:55 编辑

回复 17# plp626

在 cmd 下不能用 set/a "t=a/b+!!(a%b)" 实现 ceil(a/b)

在 cmd 下, 当 a, b 正负同号时, a/b 采取的是截尾式取整 (floor() 的效果); 但当 a, b 正负异号时, 其实和 ceil(a/b) 是等同的.
或者换一种理解方式: 将 商的绝对值按 floor 的方式取整, 再附上符号, 同取正, 异取负.

set /a 16/7
2
set /a -16/-7
2
set /a -16/7
-2
set /a 16/-7
-2

对于 floor((m*306 + 5)/10) 中的 5, 并非唯一, 只要几个常数配合上能让最后的结果得到 0,31,61,92,122,153,184,214,245,275,306,337 这样一个序列就是理想的组合, 或者说对这个序列给出了一个 完美拟合 并且 形式上足够简洁 而且易于实现 的 解析式.

m*306 + 5 得到的序列:
5,311,617,923,1229,1535,1841,2147,2453,2759,3065,3371 中 个位数最小为 1, 最大达到 9, 所以 5  换成 4, 仍然可以完美拟合, 但能和 306, 10 理想配合的除了 5 和 4 外再没有别的数字了.
作者: plp626    时间: 2012-4-9 07:49

本帖最后由 plp626 于 2012-4-9 12:13 编辑

回复 21# neorobin

是我不够严密;应该是 当a/b>0时; set/a a/b+!!(a%b) 等同于 ceil(a/b)
【set/a a/b+!!(a%b) 可进一步化简为 set/a a/b-!(a%b);】

概括来讲,set/a "a/b" 等价于fix(a/b);而ab同号时,等价于floor(a/b);ab异号时等价于ceil(a/b);
我记忆fix,floor,ceil的方式是 ---- floor  --- (fix) --- ceil --->
即floor向左取整,fix向零(原点)取整,ceil向右取整;

对于x为正的情况,用fix如何表示ceil,需要根据具体的x来变换,画函数图像可看出 ceil(x)=N+fix(x-N) (-N<x<N,N为正整数) ;

比如,当a/b所表示的实数的绝对值小于10000时; ceil(a/b)=set/a (a-10000*b)/b+10000
------------------------------------
对于那个常数;
昨天思考了其一般问题即:如何判断两组数据在整数范围上是否线性相关,若存在如何求解关系式;已有初步结果;
(更一般的问题是 寻求在整数范围上有理函数建立的关系;这个数学问题很有意义)

这些整数常数(y=(kx+b)/c ,k,b,c为整数常数) 可以通过给出的数据直接求出k/c, 和修正数b的范围;
在给定c或者k的情况下,另外的两个常数便可确定范围;需要一些代数知识(像是整数范围上的最小二乘法);
昨天贴了几张图都是和数学有关,有点离题,便又删了;

其实 (306*m+5)/10 能换成换成(306*m+4)/10,就可以化简了, (153*m+2)/5,这样代码少了1字节,致精致简;
作者: fatcat    时间: 2012-4-9 08:29

回复 22# plp626


    几种取整方式: floor(), ceil(), trunc()(或表示为 fix()), round() cmd 中最易于实现的是 trunc(): 直接截去小数部分取整.
作者: terse    时间: 2012-4-9 13:16

发出后 看到P兄已发
  1. (153*M+2)/5
复制代码

作者: neorobin    时间: 2012-4-9 14:22

本帖最后由 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,
再重新计算序号误差:
  1. set /a "y=(i-1)*250/91311+1, dd=i-(365*y + y/4 - y/100 + y/400)"
  2. 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 日的偏移
monthlenindsef(s)*1000f(e)*1000trunc(f(s))trunc(f(e))
Mar3100301399300
Apr30131601026197311
May31261912006298622
Jun303921213019396733
Jul3141221524000498044
Aug3151531835013599355
Sep3061842136026697366
Oct3172142447006798677
Nov3082452748019896788
Dec3192753059000998099
Jan311030633610013109931010
Feb281133736411026119081111
Feb(leap)291133736511026119411111


上面表格可用代码得到:
  1. @echo off & setlocal enabledelayedexpansion
  2. echo mon len ind    s     e     f(s)    f(e)  int(f(s))   int(f(e))
  3. for %%i in ("Feb 28" "Feb 29") do (
  4.   set /a "i=100, s=1000, e=1000-1"
  5.   for %%a in ("Mar 31" "Apr 30" "May 31" "Jun 30" "Jul 31" "Aug 31"
  6.   "Sep 30" "Oct 31" "Nov 30" "Dec 31" "Jan 31" %%i) do (
  7.     set "l=%%~a"
  8.     set /a "e+=!l:~-2!, fs=(5*(s-1000)+2)*1000/153+100000,fe=(5*(e-1000)+2)*1000/153+100000"
  9.     set /a "ifs=(5*(s-1000)+2)/153+100,ife=(5*(e-1000)+2)/153+100"
  10.     set "lin=!l!   !i:~-2!   !s:~-3!   !e:~-3!   !fs:~-5!   !fe:~-5!     !ifs:~-2!          !ife:~-2!"
  11.     set "lin=!lin: 0=  !"
  12.     if %%i=="Feb 28" (echo !lin!) else if %%a=="Feb 29" echo !lin!
  13.     set /a "s+=!l:~-2!, i+=1"
  14. ))
复制代码
综上, 由序号求日期的完全代码:
  1. set /a "y=(i-1)*250/91311+1,dd=i-(365*y+y/4-y/100+y/400)"
  2. set /a "y+=dd>>31,dd=i-(365*y+y/4-y/100+y/400)"
  3. set /a "mi=(5*dd+2)/153"
  4. set /a "m=(mi+2)%%12+1" & rem {0,1,2,..9,10,11} -> {3,4,5,..12,1,2}
  5. set /a "y-=m-3>>31" & rem 1,2月到了下一年
  6. set /a "d=1+dd-(153*mi+2)/5"
复制代码
PS:
计算序号的代码在开始可以将 y 加上一个 400 的倍数, 在序号转为日期的最后将 y 再减去同一个 400 的倍数, 这样就可以将年份 y 的可计算范围向负数调整. 上面 [1, 20000] 的范围分作正负数各一半仍足够大.

plp626, terse不约而同的都想到了 153, 2, 5 这个组合.

下面是一个算法互逆性测试代码, 但不能对序号生成的日期正确性作验证测试:
  1. @echo off & setlocal enabledelayedexpansion
  2. REM 7305155=20000.12.31
  3. for /l %%i in (0 1 7305155) do (
  4.   set /a "i=%%i"
  5.   set /a "y=(i-1)*250/91311+1,dd=i-(365*y+y/4-y/100+y/400)"
  6.   set /a "y+=dd>>31,dd=i-(365*y+y/4-y/100+y/400)"
  7.   set /a "mi=(5*dd+2)/153"
  8.   set /a "m=(mi+2)%%12+1" & rem {0,1,2,..9,10,11} -> {3,4,5,..12,1,2}
  9.   set /a "y-=m-3>>31" & rem 1,2月到了下一年
  10.   set /a "d=1+dd-(153*mi+2)/5"
  11.   set "test=%%i:   !y!.!m!.!d!"
  12.   set /a "m+=9,m%%=12,y-=m/10,i=365*y+y/4-y/100+y/400+(m*306+5)/10+d-1"
  13.   set "test=!test!     !i!"
  14.   if "!i!" neq "%%i" set "test=!test!     error"
  15.   echo !test!
  16.   if "!test:error=!" neq "!test!" pause
  17. )
复制代码

作者: plp626    时间: 2012-4-9 17:24

本帖最后由 plp626 于 2012-4-9 17:25 编辑

回复 25# neorobin
  1. struct sdate dtf(long d) { /* convert day number to y,m,d format */
  2.   struct sdate pd;
  3.   long y, ddd, mm, dd, mi;
  4.   y = (10000*d + 14780)/3652425;
  5.   ddd = d - (y*365 + y/4 - y/100 + y/400);
  6.   if (ddd < 0) {
  7.     y--;
  8.     ddd = d - (y*365 + y/4 - y/100 + y/400);
  9.     }
  10.   mi = (52 + 100*ddd)/3060;
  11.   pd.y = y + (mi + 2)/12;
  12.   pd.m = (mi + 2)%12 + 1;
  13.   pd.d = ddd - (mi*306 + 5)/10 + 1;
  14.   return pd;
  15.   }
复制代码
原作者的C代码;我精简后,除了开头部分,后面的和你基本一致;

你代码中的开始一行中的常数250,91311,这些如何得来的?
作者: neorobin    时间: 2012-4-9 17:35

本帖最后由 neorobin 于 2012-4-9 17:45 编辑

回复 26# plp626


    http://alcor.concordia.ca/~gpkatch/gdate-method.html

y = (10000*g + 14780)/3652425

10000 / 3652425  (相当于 除以 400 年的年平均天数 365.2425)
除以 40
为了 20000] 的界限不溢出
23519 * 91311 = 2147543409 上溢了
作者: plp626    时间: 2012-4-9 17:44

本帖最后由 plp626 于 2012-4-9 17:54 编辑

回复 27# neorobin


    这样啊,那你的代码可以再化简了
  1. ->m 1/365.2425
  2. 0.00273790701
  3. ->m 250/91311
  4. 0.00273789576
  5.  
  6. ->m 250/91310
  7. 0.00273792575
复制代码
可以看出,用91310代替91311更接近真实值;这样一来250,91310又可以化简了;同除以10;得25,9131;
----------
上面的接近分析有误,实际是:
|0.00273790701-0.00273789576| < |0.00273790701-0.00273792575|
作者: neorobin    时间: 2012-4-9 17:46

回复 28# plp626


    常数再小的话, 估值误差的范围会增大, 后面的代码不好简洁实现了
作者: neorobin    时间: 2012-4-9 18:01

本帖最后由 neorobin 于 2012-4-9 18:03 编辑

回复 28# plp626


    [1,20000]

pOffs := ((d1st - 1) * 250) / 91311 - p;

The pOffsMin is: -0.088522;  the pOffsMax is: -0.001161
The pMin is: 19903;  the pMax is: 96
pOffsMin = -0.088522,  pOffsMax + 365*250 / 91311 = 0.998171

pOffs := ((d1st - 1) * 25) / 9131 - p;

The pOffsMin is: -0.004709;  the pOffsMax is: 0.134158
The pMin is: 303;  the pMax is: 20000
pOffsMin = -0.004709,  pOffsMax + 365*25 / 9131 = 1.133501

这个误差范围, 还可以调整到 [p-1, p] 了
下面更甚

pOffs := ((d1st - 1) * 5) / 1825 - p;

The pOffsMin is: -0.002740;  the pOffsMax is: 13.284932
The pMin is: 1;  the pMax is: 20000
pOffsMin = -0.002740,  pOffsMax + 365*5 / 1825 = 14.284932
作者: neorobin    时间: 2012-4-9 18:54

本帖最后由 neorobin 于 2012-4-9 19:12 编辑

回复 28# plp626


    刚看到你回复的 30 楼, 突然不见了
测试结果
pOffs := (d1st * 10000.0) / 3652425 - p;

The pOffsMin is: -0.004045;  the pOffsMax is: 0.001971
The pMin is: 8303;  the pMax is: 8496
pOffsMin = -0.004045,  pOffsMax + 365*10000 / 3652425 = 1.001307

-0.004045 * 3652425 = -14774.059125
14780 比较恰好的调整误差范围到 [0.000000, 1.005354]:


pOffs := (d1st * 10000.0 + 14780) / 3652425 - p;

The pOffsMin is: 0.000000;  the pOffsMax is: 0.006018
The pMin is: 4;  the pMax is: 1296
pOffsMin = 0.000000,  pOffsMax + 365*10000 / 3652425 = 1.005354


pOffs := (d1st * 10000.0 -7300) / 3652425 - p;

The pOffsMin is: -0.006044;  the pOffsMax is: -0.000027
The pMin is: 8303;  the pMax is: 2096
pOffsMin = -0.006044,  pOffsMax + 365*10000 / 3652425 = 0.999309

Pascal 测试代码
  1. var
  2.   d1st, p, pMin, pMax: longint;
  3.   pOffs, pOffsMin, pOffsMax: real;
  4. begin
  5.   pOffsMin := 0;
  6.   pOffsMax := -1;
  7.   for p := 1 to 20000 do
  8.   begin
  9.     d1st := 365 * p + p div 4 - p div 100 + p div 400;
  10.     pOffs := (d1st * 10000.0) / 3652425 - p;
  11.     if pOffs < pOffsMin then begin pOffsMin := pOffs; pMin := p; end
  12.     else if pOffs > pOffsMax then begin pOffsMax := pOffs; pMax := p; end;
  13.     WriteLn(p, '  ', d1st, '  ', pOffs: 0: 6);
  14.   end;
  15.   WriteLn('The pOffsMin is: ', pOffsMin: 0: 6, ';  the pOffsMax is: ', pOffsMax: 0: 6);
  16.   WriteLn('The pMin is: ', pMin: 0, ';  the pMax is: ', pMax: 0);
  17.   WriteLn('pOffsMin = ', pOffsMin: 0: 6, ',  pOffsMax + 365*10000 / 3652425 = ', pOffsMax + 365 * 10000 / 3652425: 0: 6);
  18.   Readln;
  19. end.
复制代码

作者: neorobin    时间: 2012-4-9 20:06

回复 31# neorobin
   
http://alcor.concordia.ca/~gpkatch/gdate-method.html

. In the intervening years of this 400 year cycle, the error maxima are 1.4775 days in year 303 of the cycle, and -0.72 days in year 96 of the cycle. Because of this error, finding the year given a number of days (the inverse function) is not exact, but we can find a very close approximation. Given d days, the year number can be approximated as
y = d / 365.2425     (3)

查看 400 年周期内, 平均数估算天数的误差峰值, 在命令行运行:
  1. >nul (set /a "mi=0,ma=0,ii=1,ia=1"&(for /l %y in (1 1 400) do set /a "y=%y,d=y*365+y/4-y/100+y/400,d*=10000,d=y*3652425-d,ii^=(d-mi>>31)&(y^ii),ia^=(ma-d>>31)&(y^ia),mi^=(d-mi>>31)&(d^mi),ma^=(ma-d>>31)&(d^ma)"))&set m&set i
复制代码


ma=14775
mi=-7200
ia=303
ii=96
作者: plp626    时间: 2012-4-9 20:35

本帖最后由 plp626 于 2012-4-9 20:42 编辑

回复 30# neorobin
理论上的分析没经得起实践测试;漏掉了一个变量,分析有误,便删了;
---------------------------
date2index的反函数index2date;
应该可以精简优化到两行150个字节以内
很期待这个函数。。。
作者: Batcher    时间: 2012-4-9 20:51

回复 31# neorobin


    现在还有学校在教Pascal啊,好神奇。
作者: terse    时间: 2012-4-10 00:44

没完全测试 翻版一下网上资料及万年历
  1. @echo off&setlocal enabledelayedexpansion
  2. FOR /l %%i in (0 1 7305155) DO (
  3.     set JD=%%i
  4.     IF !JD! GEQ 2299161 set /a "JD+=1+(JD*100-186721625)/3652425-(JD*100-186721625)/3652425/4"
  5.     set /a "B=JD+1524,Y=(B*100-12210)/36525,D=36525*Y/100"
  6.     set /a "M=(B-D)*10000/306001,D=B-D-306001*M/10000,M=(M-2)%%12+1,Y-=4715+^!(2/M)"
  7.     set "test=%%i:   !y!.!m!.!d!"
  8.     set /a "M=(M+9)%%12+3,Y-=M/13,JD=36525*(Y+4716)/100+306001*(M+1)/10000+D-1524"
  9.     set /a "JD+=(2-Y/100+Y/400)*^!(2299161/JD)"
  10.     set "test=!test!     !JD!"
  11.     if "!JD!" neq "%%i" set "test=!test!     error"
  12.     echo !test!
  13.     if "!test:error=!" neq "!test!" pause
  14. )
  15. PASUE
复制代码

作者: neorobin    时间: 2012-4-10 01:03

回复 33# plp626

把 25 楼的代码改了一点点, 没有实质性的变化, 凑成 2 行 155 字节:
  1. set /a "y=(i*99+145)/36159,y+=i-365*y-y/4+y/100-y/400>>31,dd=i-365*y-y/4+y/100-y/400"
  2. set /a "mi=(5*dd+2)/153,m=(mi+2)%%12+1,y-=m-3>>31,d=1+dd-(153*mi+2)/5"
复制代码

作者: neorobin    时间: 2012-4-10 02:33

本帖最后由 neorobin 于 2012-4-10 02:54 编辑

回复 35# terse


    不错, 我一直测试到 7305155 没有发现错误
作者: neorobin    时间: 2012-4-10 02:40

回复 34# Batcher

事实上我没有上过电脑课, Pascal: 端庄典雅秀丽的淑女; C: 自由奔放不羁的野马
作者: plp626    时间: 2012-4-10 13:31

回复 36# neorobin

其实你很容易就能精简到140字节以内。。。
作者: neorobin    时间: 2012-4-10 14:54

回复 39# plp626


    弄在一行了, 不必要的变量都省了, 137 字节
  1. set /a "y=(i*99+145)/36159,y+=i-365*y-y/4+y/100-y/400>>31,d=i-365*y-y/4+y/100-y/400,m=(5*d+2)/153,d+=1-(153*m+2)/5,y+=m/10,m=(m+2)%%12+1"
复制代码

作者: plp626    时间: 2012-4-10 16:06

有一个问题
如何测试可以断定date2index一定正确?
如何测试可以断定index2date一定正确?

在date2index都不能断定是正确的情况下,
那么,测试index2date正确性的时候,去调用date2index得出的结论应该是不确定的。。
作者: neorobin    时间: 2012-4-10 17:03

本帖最后由 neorobin 于 2012-4-10 17:42 编辑

回复 41# plp626
  1. @echo off & setlocal enabledelayedexpansion
  2. REM nextIndex := date2index(1.1.1);
  3. for /l %%y in (1 1 20000) do (
  4.   set "y=%%y"
  5.   set /a "leap = (^!(%%y %% 4) & ^!^!(%%y %% 100)) | ^!(%%y %% 400)"
  6.   if "!leap!"=="1" (set "eFeb=29") else set "eFeb=28"
  7.   for %%t in ("1 31" "2 !eFeb!" "3 31" "4 30" "5 31" "6 30" "7 31" "8 31" "9 30" "10 31" "11 30" "12 31") do (
  8.     for /f "tokens=1,2" %%m in ("%%~t") do (
  9.       set "m=%%m"
  10.       for /l %%d in (1 1 %%n) do (
  11.         set "d=%%d"
  12.         REM testing...
  13.         REM if date2index(y.m.d) <> nextIndex (
  14.           REM error
  15.         REM )
  16.         REM if index2date(date2index(y.m.d)) <> y.m.d (
  17.           REM error
  18.         REM )
  19.         REM nextIndex += 1;
  20.         echo !y!.!m!.!d!
  21.       )
  22.     )
  23.     pause
  24.   )
  25. )
复制代码
和 13 楼说的差不多, 我没想到可以证明正确的方式:

设想一个并不聪明的一定范围内穷举式的测试方式:

公历的历法是明确的:
每年 12 个月: m∈[1..12]
4,6,9,11 月份固定 30 天, d ∈[1..30]
2 月根据年份平闰取 28 天或 29 天,  d∈[1..29]
其余月份固定 31 天, d∈[1..31]

闰年:
(被 4 整除 且 不被 100 整除) 或 被 400 整除 的年份
闰年外的年份都是平年

从这个公历历法的原型定义出发设计一个 尽可能直接符合 且 足够简单 的日期生成算法(假定它是正确的算法):
从一个初始日期开始, 依次生成每一个明天,
把这个算法生成的日期作为 date2index 的参数, 得到相应的 序号:

i_0: 初始日期的序号(值由算法自定);   (_0 表示 0 为下标)

i_p: 初始日期 或者 初始日期之后的 某个日期的序号;
i_q: i_p 对应的那个日期的下一日的序号;

那么, 如果对于任意 i_p 与 i_q, 都得到:

i_q = i_p + 1

就可以把 date2index 看作是测试范围内正确的算法.

在上面的测试中可以同时 作 index2date 测试:
如果始终都有:
index2date( date2index(y.m.d) ) = y.m.d

在 date2index 测试正确的情况下, 就可以认为 index2date 也是正确的.


date2index 和 index2date 在任意参数的情况下的输出都是唯一的, 且是确定的(对于一个确定的输入 I, 不会得到 O1, O2两种或更多可能的输出),

因为算法是基于 数字计算机 的, 输入参数 不具备不确定性(每次输入的值的集合都是确定的),
且算法中不涉及随机函数的调用, 所以 输出是 具有 唯一性和确定性的.
作者: plp626    时间: 2012-4-10 19:23

bat测试太慢;
set/a 表达式等同C的算术表达式;
这里给个超级精简版的C编译器,方便大家测试用;
[attach]5113[/attach]
作者: terse    时间: 2012-4-11 13:05

回复 42# neorobin
以此代码推算至100.2.28后就是100.3.1按实历算的话应该是100.2.29后才是100.3.1
这里是否考虑实历1582前后问题 即1582前每4年一闰  1582后才多了100和400余
也就是说如果这里仅仅代表一种序号的话 那这个序号的合理性应该是计算1582年10月15号后的
譬如 1582年10月4号 儒略日 (2299160)  后一日 便直接跳至1582年10月15日 儒略日 (2299161)
也可能个人理解的不同  故一点浅见而已
作者: neorobin    时间: 2012-4-11 13:43

回复 44# terse

惭愧, 对儒略日, 历法变化等问题完全没有了解或者了解甚少, 前面都完全是按一个固定的历法规则处理的, 如要适应相关的 规则约定 或者 实际情况, 肯定要作出进一步的修改.
作者: plp626    时间: 2012-4-11 18:36

回复 44# terse




    100-2-28 的下一天是 100-2-29 还是 100-3-1

如果是前者,那就不是现在的历法,背后有什么故事么。。。?
作者: plp626    时间: 2012-4-11 18:59

本帖最后由 plp626 于 2012-4-11 20:59 编辑

回复 40# neorobin
  1. @echo off
  2. :date2i <year> <month> <day> // 显示year-month-day 所对应的索引值;
  3. setlcoal&set/a y=%1,m=%2,d=%3
  4. set/a m+=9
  5. set/a m%%=12
  6. set/a y-=m/10
  7. set/a i=365*y+y/4-y/100+y/400+(m*153+2)/5+d-1
  8. endlocal&echo %i%
复制代码
  1. @echo off
  2. :i2date <index> // 显示 %1 所对应的日期 %1 取值范围0~2千万
  3. setlocal&set/a i=%1
  4. set/a y=(i*99+145)/36159
  5. set/a t=i-y*365-y/4+y/100-y/400
  6. set/a "y+=t>>9"
  7. set/a t=i-y*365-y/4+y/100-y/400  
  8. set/a m=(t*5+2)/153
  9. set/a d=t-(m*153+2)/5
  10. set/a d=d+1
  11. set/a y+=(m+2)/12
  12. set/a m=(m+2)%%12+1
  13. endlocal&echo %y%-%m%-%d%
复制代码
在索引号转日期的函数i2date中,(i*99+145)/36159
这个组合保证 [0~21691753)以内的索引;即[0-3-1 ~ 59208-9-2);之间的正确性;

(i*4+999)/1461 这个组合可以保证 [0~12200561 )以内的索引; 即[0-3-1 ~ 33404-3-1)之间的所有日期;

进一步发现
i*33/12053
这个组合可以保证[0~65075232) 以内;即[0-3-1 ~ 177625-3-7) 之间的所有日期;
error: /65075263:177625/ 9/ 9

以上结论只代表在假定函数date2index正确的情况下,没有报错的测试结果;

如果正确,date2index和 index2date两个函数轻松搞定了有关时间日期计算(0-3-1后)的所有问题;
作者: plp626    时间: 2012-4-11 20:06

本帖最后由 plp626 于 2012-4-11 21:02 编辑

C测试,40万年以内测试用时秒杀
  1. //&cls&@type %~fs0|tcc -run -
  2. // 把tcc.exe 和 lib\msvcrt.def 放在当前目录下;双击本脚本解释执行下面C代码
  3. int i2date(int i, int *, int *, int *);
  4. int date2i(int, int, int);
  5. /*
  6. * 把0-3-1作为参考日历{0-0-0}的0号索引,后面依次类推;
  7. * 参考日历月份数为 0,1,2,...,11;
  8. * 11月分闰月和平月;当所在年份+1为闰年时为闰月;
  9. * 参考日历日期数为 0,1,...,30;
  10. * 大月最大偏移30,小月最大偏移29;
  11. * 11月中,闰月最大偏移28;平月最大偏移27;
  12. */
  13. int main(){
  14. int i,j;
  15. int y,t,m,d;
  16. for (i=0; i<1461*100000; i++){ // 0 ~ 40万年索引号
  17. i2date(i,&y,&m,&d);
  18. j=date2i(y,m,d);
  19. if (i!=j) { // 测试,寻找不对称转换的具体值。。
  20. printf("/%5d:%4d/%2d/%2d\n",i,y,m,d);
  21. getchar();
  22. }
  23. }
  24. return 0;
  25. }
  26. int i2date(int i, int *year, int *month, int *day){ // 索引转日期
  27. int t,y,m,d;
  28. y=(i*4+999)/1461;          // 1/365.2425的前6位小数最佳有理逼近
  29. // y=(i*99+145)/36159;
  30. // y=i*33/12053;            //1/365.2425的前9位小数最佳有理逼近
  31. t=i-y*365-y/4+y/100-y/400;
  32. y+=t>>9; // 获得参考年份;若t为负数,将参考年减1
  33. t=i-y*365-y/4+y/100-y/400;  // 获得参考年0月0日的偏移数
  34. m=(t*5+2)/153; // 获得参考月份
  35. d=t-(m*153+2)/5; // 获得参考日期
  36. d=d+1; // 获得实际日期
  37. y+=(m+2)/12; // 获得实际年份
  38. m=(m+2)%12+1; // 获得实际月份
  39. *year=y;
  40. *month=m;
  41. *day=d;
  42. return 0;
  43. }
  44. int date2i(int y, int m, int d){// 日期转索引
  45. int i;
  46. m+=9;
  47. m%=12;
  48. y-=m/10;
  49. //上面三句做平移取模,3月作为当年的0月;2月作为上1年的11月;
  50. i=365*y+y/4-y/100+y/400+(m*153+2)/5+d-1;
  51. // 11月初的偏移为337;337/11≈153/5;
  52. // (m*153+2)/5 将 {0,1,2,...,11}映射为{0,31,61,...,337}
  53. return i;
  54. }
复制代码
可是,这样的对称测试 有意义吗? 即使在date2index正确的条件下,也存在index2date出错;

即使date2index正确,date2index它也只是保证正确的日期生成正确的索引;
错误的日期也有可能生成正确的索引;2002-2-29号是错的,但它在意义上式2002-3-1;生成的索引也是2002-3-1日的索引;
如此以来,还需验证index2date生成正确的日期。。。
作者: plp626    时间: 2012-4-11 21:52

进一步测试发现,i2date函数很是问题;

bug丛生;

问题出在 i2date函数的第一句; 不是随便可以精简的。。。
作者: neorobin    时间: 2012-4-11 21:59

回复 48# plp626

这里不考虑 terse 提出的历法变化问题,

42 楼给出的测试方式是 假定一个 简单易懂易实现的 日期生成 算法 是正确的, 用它从 某个日期 开始依次生成某范围内所有后续的日期.
这样生成的一个日期序列的序号对应着整数序列的一部分,

index = date2index(测试起始日期)
loop (简单算法生成 y.m.d,  直到测试终止日期, (y.m.d)++) {
  y.m.d   ->  i = date2index(y.m.d)   ->   yy.mm.dd = index2date(i)
  上过程中: (i ≠ index)  或者 (yy.mm.dd ≠ y.m.d) 即发现错误
  index++
}

bat 测试确实很慢, 还好没事开着不管它, 写成高级语言的也容易, 下面是已经完成测试过的 bat:
  1. @echo off & setlocal enabledelayedexpansion
  2. REM Index := date2index(1.1.1);
  3. set /a "y=1,m=1,d=1"
  4. set /a "m+=9, m%%=12,y-=m/10, i=365*y + y/4 - y/100 + y/400 + (m*306 + 5)/10 + d - 1"
  5. set /a "Index=i"
  6. for /l %%y in (1 1 20000) do (
  7.   set "y=%%y"
  8.   set /a "leap = (^!(%%y %% 4) & ^!^!(%%y %% 100)) | ^!(%%y %% 400)"
  9.   if "!leap!"=="1" (set "eFeb=29") else set "eFeb=28"
  10.   for %%t in ("1 31" "2 !eFeb!" "3 31" "4 30" "5 31" "6 30" "7 31" "8 31" "9 30" "10 31" "11 30" "12 31") do (
  11.     for /f "tokens=1,2" %%m in ("%%~t") do (
  12.       set "m=%%m"
  13.       for /l %%d in (1 1 %%n) do (
  14.         set "d=%%d"
  15.         REM testing...
  16.         REM if date2index(y.m.d) <> Index (
  17.           REM error
  18.         REM )
  19.         REM if index2date(date2index(y.m.d)) <> y.m.d (
  20.           REM error
  21.         REM )
  22.         REM Index += 1;
  23.         set /a "m+=9, m%%=12,y-=m/10, i=365*y + y/4 - y/100 + y/400 + (m*306 + 5)/10 + d - 1"
  24.         if "!i!" neq "!Index!" ((echo error at %%y.%%m.%%d date2index(y.m.d^)=!i!^<^> !Index!) & pause)
  25.         set /a "y=(i*99+145)/36159,y+=i-365*y-y/4+y/100-y/400>>31,d=i-365*y-y/4+y/100-y/400,m=(5*d+2)/153,d+=1-(153*m+2)/5,y+=m/10,m=(m+2)%%12+1"
  26.         set /a "error=(y-%%y)|(m-%%m)|(d-%%d)"
  27.         if "!error!" neq "0" ((echo error at %%y.%%m.%%d index2date(date2index(y.m.d^)^)=!y!.!m!.!d!) & pause)
  28.         echo %%y.%%m.%%d  Index=!Index!
  29.         set /a "Index+=1"
  30.       )
  31.     )
  32.   )
  33. )
  34. pause
  35. exit
复制代码

作者: neorobin    时间: 2012-4-11 22:07

本帖最后由 neorobin 于 2012-4-11 23:02 编辑

回复 49# plp626

31 楼, 我做了年份估值误差范围测试, 如果估值函数在给定计算范围内的 估值误差范围 跨过 两个整数(比如 [-0.1,1.3] 跨过了 0 和 1 两个整数), 就不利于后续代码, 后面年份的误差调整就不是简单的只作出 要么不变, 要么加1(或者减1) 这样的方式了, 因为 跨过两个整数 意味着误差的整数绝对值跨度将达到 2.
作者: terse    时间: 2012-4-11 22:15

回复 46# plp626
100-2-28 的下一天是 100-2-29
这里说的公元100年2月应该有29天
前面我提到 公元1582年10月4号前是每4年一闰  不管年份是否可被100或400整除
1582年10月由格勒哥里十三世改革成为格勒哥里历,取消1582年10月5日至1582年10月14日这10日及取消400年内00年尾的3个闰年,使一年的平均日数变成365.2425日,更接近于准确的回归年365.2422日。
格勒哥里历(也就是现在大多使用的):
每4年置闰年一次,闰年366日,二月29日.凡年份数可被4 整除者,置闰. 如1960,1996等.凡年份数可被100整除者,不置闰, 如1800,1900.凡年份数可被400整除者,置闰, 如2000,2400.
作者: plp626    时间: 2012-4-11 22:38

回复 52# terse


不同年份阶段,要不同的历法,    太复杂了; 很不好计算;

反正matlab里面100-2-28的下一天是100-3-1;
  1. >> datestr(36584,26)
  2. ans =
  3. 0100/02/28
  4. >> datestr(36585,26)
  5. ans =
  6. 0100/03/01
  7. >>
复制代码

作者: neorobin    时间: 2012-4-11 22:43

本帖最后由 neorobin 于 2012-4-11 23:10 编辑

回复 53# plp626


    用个能算到 公元 3000 年的历法就蛮足够了, 往过去可以算 100 年也就可以了, 我们不考古, 呵呵,  我用过最大的用途就是看下出生日期什么的.


51 楼纠正一下, 应说成 整数误差绝对值 跨度 将达到 2: 例如, 应该得到的 年份是 2012, 但误差范围可能出现 3 种(据算法择其中一种)可能的区间 [2010,2012], [2011,2013], [2012,2014], 尽管这个误差正峰值和负峰值并不会发生在对同一个年份的估值上, 但代码上总是要对这两个峰值作统一处理的.
作者: plp626    时间: 2012-4-11 23:03

i2date中,
  1. y=(i*99+145)/36159;
复制代码
通过9999年以内的索引转日期测试;
我用matlab生成标准日期文件69M+;
再用测试代码用C生成了标准日期文件69M+;
两个文件md5值相同均为 a6740e69a45b825334b8c4c4c986fe2f;

标准打印格式:

fid 模式wt;
fprintf(fid,"%7d:%.4d/%.2d/%.2d\n",i,y,m,d);
末行 3652060:9999/03/02

索引范围 0~3652060 [0-3-1 ~ 9999-3-2]
作者: terse    时间: 2012-4-11 23:08

针对1582 10 4 ----1582 10 15 的测试
  1. @echo off&setlocal enabledelayedexpansion
  2. (FOR /l %%i in (2298883 1 2299603) DO (
  3.     set JD=%%i
  4.     IF !JD! GEQ 2299161 set /a "JD+=1+(JD*100-186721625)/3652425-(JD*100-186721625)/3652425/4"
  5.     set /a "B=JD+1524,Y=(B*100-12210)/36525,D=36525*Y/100"
  6.     set /a "M=(B-D)*100/3061,D=B-D-3061*M/100,M=(M-2)%%12+1,Y-=4715+^!(2/M)"
  7.     set "str=!Y!:!M!:!D!"
  8.     set /a "M=(M+9)%%12+3,Y-=M/13,JD=36525*(Y+4716)/100+3061*(M+1)/100+D-1524"
  9.     REM 1582年10月4日后重新计算闰年,先前每4年一闰.
  10.     set /a "JD+=(2-Y/100+Y/400)*^!(2299161/JD)"
  11.     ECHO !str! !JD!
  12. ))>Yjd.txt
  13. start "" "Yjd.txt"
  14. PAUSE
复制代码
  1. @echo off&setlocal enabledelayedexpansion
  2. for %%i in (31 28 31 30 31 30 31 31 30 31 30 31) do set /a N+=1&set "M_!N!=%%i"
  3. set JDX=2298883
  4. for /l %%i in (1582 1 1583) do (
  5.      IF %%i gtr 1582 (
  6.         set /a "N=^!(%%i%%4)^^^!(1582/%%i)&^!(%%i%%400)^^^!(1582/%%i)&^!(%%i%%100)"
  7.      ) else set /a "N=^!(%%i%%4)"
  8.      set /a M_2=28+N
  9.      for /l %%j in (1 1 12) do (
  10.          set /a "M=(%%j+9)%%12+3,Y=%%i-M/13"
  11.          for /l %%k in (1 1 !M_%%j!) do (
  12.              set /a "JD=36525*(Y+4716)/100+3061*(M+1)/100+%%k-1524,JDX+=1"
  13.              set /a "JD+=(2-Y/100+Y/400)*^!(2299161/JD)"  2>nul
  14.              IF !JDX! NEQ !JD! set ERR=!err!$err:%%i:%%j:%%k !JD! !JDX!$
  15.              REM 1582年10月4日后初始日须减10天
  16.              if "%%i:%%j:%%k" == "1582:10:5" set/a JDX-=10
  17.          )
  18.      )
  19. )
  20. IF DEFINED ERR echo !err:$=^
  21. !
  22. ECHO !JD! !JDX!
  23. pause
复制代码

作者: plp626    时间: 2012-4-11 23:11

回复 51# neorobin


    我相信那个比值 (33*i+x)/12053可代替 (i*99+145)/36159;且范围不会大幅缩减;

找3000年以内的;我考虑 (4*i+x)/1461
作者: neorobin    时间: 2012-4-11 23:29

本帖最后由 neorobin 于 2012-4-11 23:32 编辑

回复 57# plp626


    我测试的结果是 [1,3000] 内,  (i * 4 + 93) / 1461 就可行

(i * 4 + 0) / 1461
The pOffsMin is: -0.062971;  the pOffsMax is: 0.000000
The pMin is: 3000;  the pMax is: 4
pOffsMin = -0.062971,  pOffsMax + 365*4 / 1461 = 0.999316

0.062971 * 1461 = 92.000631

(i * 4 + 93) / 1461
The pOffsMin is: 0.000000;  the pOffsMax is: 0.063655
The pMin is: 4;  the pMax is: 4
pOffsMin = 0.000000,  pOffsMax + 365*4 / 1461 = 1.062971
作者: plp626    时间: 2012-4-11 23:53

你测试 (i * 4 + 93) / 1461 兼容的最大范围是?
作者: neorobin    时间: 2012-4-12 00:01

回复 59# plp626

测试代码确实有问题, 是记录 误差最小值的一个变量的初始值设小了,  但 93 这个常数还是碰对了

修改后:
   
93
The pOffsMin is: 0.000684;  the pOffsMax is: 0.063655
The pMin is: 3000;  the pMax is: 4
pOffsMin = 0.000684,  pOffsMax + 365*4 / 1461 = 1.062971



0
The pOffsMin is: -0.062971;  the pOffsMax is: 0.000000
The pMin is: 3000;  the pMax is: 4
pOffsMin = -0.062971,  pOffsMax + 365*4 / 1461 = 0.999316

0.062971 * 1461 = 92.000631

第一次出错的发生:
3002.2.28  Index=1096456
error at 3002.3.1 index2date(date2index(y.m.d))=3002.2.29
作者: plp626    时间: 2012-4-12 00:22

回复 60# neorobin


    我的测试结果和你不一致;问题比较大。。。
作者: plp626    时间: 2012-4-12 00:58

本帖最后由 plp626 于 2012-4-12 14:55 编辑

加入日期正确性验证;17万年内顺利通过验证;
完美组合:
  1. y=(i*33+999)/12053 //索引反求日期中年份的
复制代码
  1. int isdate(int y, int m, int d){
  2. int mi[]={0,31,28,31,30,31,30,31,31,30,31,30,31};
  3. if ((y%4 == 0) && (y%100 != 0) || (y%100 == 0))
  4. mi[2]+=1;
  5. if (m<=0||m>12)
  6. return 0;
  7. if (d<=0||d>mi[m])
  8. return 0;
  9. return 1;
  10. }
复制代码
  1. (i*4+99)/1461; 3301-2-28以内正确;
复制代码
  1. (i*4+999)/1461; 33301-2-28以内正确;
复制代码
---------------

需要说明的是最上面的报错测试,17万年出错是因为cmd 的最大数字限制;2147483647 溢出导致的报错;
作者: neorobin    时间: 2012-4-12 01:06

回复 64# plp626

如果只是以 由序号得到的日期是否合法来测试 index2date, 我认为是不妥的:

比如: 由 i 得到了 2012.4.10, 而接着 i+1 得到的却是 2011.4.11, ??? 日期都是合法的, 但序列乱了, 也许可以证明出现 序列乱了 也必然出现非法日期, 但至少现在没有证明这一点
作者: plp626    时间: 2012-4-12 01:11

本帖最后由 plp626 于 2012-4-12 15:34 编辑

序号是单调增,也就是说日期必须逐一累加才能得到序号;

如果日期有跳跃,那前面的日期必然出现天数多了出来;
只需验证i2date生成的日期正确即可:
  1. //&cls&@type %~fs0|tcc -run -
  2. // 把tcc.exe 和 lib\msvcrt.def 放在当前目录下;双击本脚本解释执行下面C代码
  3. int i2date(int , int *, int *, int *);
  4. int date2i(int, int, int);
  5. int isdate(int, int, int);
  6. int main(){
  7. int i,j,y,t,m,d;
  8. for (i=0; i<1461*100000; i++){ // 0 ~ 40万年索引号
  9. i2date(i,&y,&m,&d);
  10. if (!isdate(y,m,d)){
  11. printf("wrongdate: /%5d:%4d/%2d/%2d\n",i,y,m,d);
  12. getchar();
  13. }
  14. j=date2i(y,m,d);
  15. if (i!=j) {
  16. printf("/%5d:%4d/%2d/%2d\n",i,y,m,d);
  17. getchar();
  18. }
  19. }
  20. return 0;
  21. }
  22. int isdate(int y, int m, int d){
  23. int mi[]={0,31,28,31,30,31,30,31,31,30,31,30,31};
  24. if ((y%4 == 0) && (y%100 != 0) || (y%100 == 0))
  25. mi[2]+=1;
  26. if (m<=0||m>12)
  27. return 0;
  28. if (d<=0||d>mi[m])
  29. return 0;
  30. return 1;
  31. }
  32. int i2date(int i, int *year, int *month, int *day){ // 索引转日期
  33.    int t,y,m,d;      y=(i*33+999)/12053;
  34.     t=i-y*365-y/4+y/100-y/400;  y+=t>>9;  t=i-y*365-y/4+y/100-y/400;
  35.     m=(t*5+2)/153;  d=t-(m*153+2)/5+1;
  36.     y+=(m+2)/12;  m=(m+2)%12+1;
  37.    *year=y;  *month=m;  *day=d;
  38.     return 0;
  39. }
  40. int date2i(int y, int m, int d){// 日期转索引
  41. m+=9;m%=12; y-=m/10;
  42. return 365*y+y/4-y/100+y/400+(m*153+2)/5+d-1;
  43. }
复制代码

作者: neorobin    时间: 2012-4-12 01:21

回复 66# plp626


{1,2,3} <--> {2012.4.12, 2012.4.16, 2012.4.30}

形如上面的互逆映射关系, 也许这里我们讨论的算法不会形成, 但如果某算法造成这样的关系, 那么不能保证日期的连续增 1 的检测方式是检测不出那种算法的错误的.
作者: plp626    时间: 2012-4-12 01:38

有道理。。。
作者: terse    时间: 2012-4-12 01:42

本帖最后由 terse 于 2012-4-12 12:49 编辑

折腾于2月的序列  是一律按现历计算吗  

先前的代码先删吧
作者: plp626    时间: 2012-4-12 15:14

本帖最后由 plp626 于 2012-4-12 15:29 编辑

回复 69# terse


    最好别删,你刚发的代码我还没来得及消化。。。(看你代码有些像现历转儒略历,和儒略历转现历;)

我在看资料儒略日对年数的累加是乘以系数365.25;现历的一年则是365.2425天;(天文上的一年则是365.24219...)
1天时间86400秒;儒略日的一天的时间相比现历的一天时间 365.2425*86400/365.25-86400=-1.78秒
这个误差在现历中,100年左右就马上体现出来;然后要弥补误差就要跳跃日期;

所以我认为仅仅的儒略日来算时差适用范围在百年内;

现历计算时间差;从天文精确时间算,2700年以内的时间差计算误差不超过1天;
作者: terse    时间: 2012-4-12 16:15

回复 70# plp626
在56楼 我对 现历转儒略历,和儒略历转现历 发的测试代码
如你所说 我想 365.25 和365.2425 差距是否就是儒略历 一直4年一闰  现历也就是(格历)比较 儒略历 应是 400年 少3 闰
我不知道现在讨论的 是儒略历和现历的互转 还是以现历 序号的互转
如是序号现历的互转 是否看少个 1582年10月的判断 只需每400年多一闰
把每年的3月 看做是一年的开始 然后2月放在做后 管他28天还是29天 反正算来是最后月了 最后按4年一个循环计算也就是 1461 天
感觉算法应该差不多 只要有一个统一下序号是按儒略历或者现历来排列即可吧
现历主要有4年一闰的麻烦(所以有了+Y/400) 而儒略历似乎看作更简便点
作者: plp626    时间: 2012-4-12 16:44

本帖最后由 plp626 于 2012-4-12 16:46 编辑

是现历;

儒略日这个需要点背景知识,一般人也没听过;

我在想我们讨论的日期计算是否分两个版本;

毕竟生活中用到的日期计算及其罕见跨千年的,那两个版本:

实用版: 1980年后的近50年; (主要是根据要求删除或寻找指定时间内的文件,还有倒计时灯 使用)

数值计算版: 公元元年前后的3000年; (研究算法,娱乐使用)
作者: fatcat    时间: 2012-4-12 22:22

回复 1# plp626

如果 代码中 使用 ">>31" 就有必要说明是针对 32 位有符号整数的, 或者将其改为 ">>63", 或者其它.
作者: plp626    时间: 2012-4-12 22:30

回复 73# fatcat


    是的;这个是32位;64位的未测试;

就本代码中遇到的情况,因 366 < 2^9=512,把31改为9完全可以;代码还上了1字节;
作者: CrLf    时间: 2012-4-13 15:06

本帖最后由 CrLf 于 2012-4-13 15:07 编辑

太棒了,佩服得五体投地
相比之下,我那笨办法是在是丢人死了...
作者: plp626    时间: 2012-11-16 23:09

bug, bug!
到底是谁的算法错了???

nginx 关于日期计算的源代码:
  1.     /*
  2.      * shift new year to March 1 and start months from 1 (not 0),
  3.      * it is needed for Gauss' formula
  4.      */
  5.     if (--month <= 0) {
  6.         month += 12;
  7.         year -= 1;
  8.     }
  9.     /* Gauss' formula for Grigorian days since March 1, 1 BC */
  10.     time = (uint64_t) (
  11.             /* days in years including leap years since March 1, 1 BC */
  12.             365 * year + year / 4 - year / 100 + year / 400
  13.             /* days before the month */
  14.             + 367 * month / 12 - 30
  15.             /* days before the day */
  16.             + day - 1
  17.             /*
  18.              * 719527 days were between March 1, 1 BC and March 1, 1970,
  19.              * 31 and 28 days were in January and February 1970
  20.              */
  21.             - 719527 + 31 + 28) * 86400 + hour * 3600 + min * 60 + sec;
复制代码
/src/http/ngx_http_parse_time.c
[attach]5881[/attach]
作者: plp626    时间: 2012-11-16 23:10

注意看这部分:
  1.   /* days in years including leap years since March 1, 1 BC */
  2.              365 * year + year / 4 - year / 100 + year / 400
  3.             /* days before the month */
  4.             + 367 * month / 12 - 30
  5.             /* days before the day */
  6.             + day - 1
复制代码

作者: BAT-VBS    时间: 2012-11-16 23:29

回复 77# plp626


    能给个例子证明一下不?
作者: plp626    时间: 2012-11-20 22:26

研究了下,两个算法等价,都正确
  1. :date2i <year> <month> <day> <RetVarName>  // 一楼计算公式
  2. setlocal&set/a y=%1,m=%2,d=%3
  3. set/am+=9,m%%=12,y-=m/10,i=365*y+y/4-y/100+y/400+(m*153+2)/5+d-1
  4. endlocal&if %4.==. (echo %i%)else set %4=%i%
  5. goto:eof
复制代码
  1. :Gauss_date2i <year> <month> <day> [RetVarName]  // Gauss 日期计算公式
  2. setlocal&set/a "y=%1,m=%2-2,d=%3
  3. if %m% leq 0 set/a m+=12,y-=1
  4. set/a i=365*y+y/4-y/100+y/400+367*m/12+d-31
  5. endlocal&if %4.==. (echo %i%)else set %4=%i%
复制代码

作者: BAT-VBS    时间: 2012-11-22 14:52

回复 79# plp626


    哦,我还以为真的有bug呢
作者: taofan712    时间: 2017-3-2 00:09

再次向前辈致敬,这些东西如果每个人都自己去想去算去写,真的麻烦。
但胡乱复制网上经验又不放心。
这里居然有这么优质的资料分享和经验交流!
阅读完毕,心情无以言表。




欢迎光临 批处理之家 (http://www.bathome.net/) Powered by Discuz! 7.2