[新手上路]批处理新手入门导读[视频教程]批处理基础视频教程[视频教程]VBS基础视频教程[批处理精品]批处理版照片整理器
[批处理精品]纯批处理备份&还原驱动[批处理精品]CMD命令50条不能说的秘密[在线下载]第三方命令行工具[在线帮助]VBScript / JScript 在线参考
返回列表 发帖

[日期时间] [已更新]批处理单行版时间日期计算 算法讨论

本帖最后由 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
复制代码
2

评分人数

    • CrLf: 有意义,有技术PB + 15 技术 + 2
    • Batcher: Good work!技术 + 1

本帖最后由 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。。。
复制代码
---------------------------------------------

TOP

我先来:
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
复制代码

TOP

本帖最后由 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中实现挺繁琐;
附件: 您需要登录才可以下载或查看附件。没有帐号?注册

TOP

大家 讨论下 儒略日 的思想,为何 “参考日期(第零天)是 1858 年 11 月 17 日”

TOP

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

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

TOP

本帖最后由 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字节,致精致简;

TOP

本帖最后由 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,这些如何得来的?

TOP

本帖最后由 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|

TOP

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

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

TOP

回复 36# neorobin

其实你很容易就能精简到140字节以内。。。

TOP

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

在date2index都不能断定是正确的情况下,
那么,测试index2date正确性的时候,去调用date2index得出的结论应该是不确定的。。

TOP

bat测试太慢;
set/a 表达式等同C的算术表达式;
这里给个超级精简版的C编译器,方便大家测试用;
附件: 您需要登录才可以下载或查看附件。没有帐号?注册
1

评分人数

    • neorobin: 谢谢,可是我已经用 BAT 测试到 17700 多了, ...技术 + 1

TOP

回复 44# terse




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

如果是前者,那就不是现在的历法,背后有什么故事么。。。?

TOP

本帖最后由 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后)的所有问题;

TOP

返回列表