forked from nillerusr/source-engine
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathshotmaker.pl
955 lines (716 loc) · 28.8 KB
/
shotmaker.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
#!perl
$rdir=shift || &printargs;
$map=shift || &printargs;
$mod=shift || &printargs;
$startdate=shift || &printargs;
$enddate=shift || &printargs;
$dateinc=shift || &printargs;
die "bad date format $startdate" unless $startdate=~s@^(\d\d\d\d)/(\d\d)/(\d\d)$@\1\2\3@;
die "bad date format $enddate" unless $enddate=~s@^(\d\d\d\d)/(\d\d)/(\d\d)$@\1\2\3@;
$jday=MJD($startdate);
$jday1=MJD($enddate);
for($day=$jday;$day<=$jday1;$day+=$dateinc)
{
($y, $m, $d)=DJM($day);
$p4cmd="p4 sync $rdir\\...\@$y/$m/$d:01:00:00 >nul 2>&1";
$hl2cmd="$rdir\\hl2 -game $mod -sw +map $map -makedevshots -dev -width 1024 -height 768";
print "Taking shots for $m/$d/$y\n";
print "$p4cmd\n";
print `$p4cmd`;
print "hl2cmd\n";
print `$hl2cmd`;
}
sub printargs
{
print STDERR "format is SHOTMAKER.PL rootdir mapname mod startdate enddate dateincrement\n";
print STDERR "ex:\nSHOTMAKER u:\\dev\\valvegames\\main\\game ep1_c17_01 episodic 2005/10/01 2005/10/05 7\n";
die;
}
# Toby Thurston --- 12 May 2003
use strict;
use Carp;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS @mon @dom);
require Exporter;
@ISA = qw(Exporter);
$VERSION = '0.03';
=head1 NAME
Cal::Date - a simple set of calendar functions for Perl
(yes, yes, I know about L<Date::Calc> and L<Date::Manip> but mine is
simpler, and nicer :-).
=head1 SYNOPSIS
use Cal::Date qw(DJM MJD today);
$date = $ARGV[0] || today();
print "$date --> " . MJD($date) . "\n";
print "Day after -->" . DJM(MJD($date)+1) . "\n";
=head1 DESCRIPTION
A simple compact interface to some simple calendar routines.
Implemented purely in Perl, no need for external C code etc.
=head1 FUNCTIONS
No functions are exported by default.
=cut
@EXPORT = qw();
=pod
The following functions can be exported from the C<Cal::Date> module:
MJD DJM
Easter old_style_Easter orthodox_Easter
ISO_week ISO_day ISO_week_and_day
day_of_year days_to_go
days_in_month
UK_tax_week UK_tax_month
working_days
today now
J2G
v_date r_date
adjust_to_local_time adjust_to_UTC
is_a_date
=cut
@EXPORT_OK = qw(
MJD DJM
Easter old_style_Easter orthodox_Easter
ISO_week ISO_day ISO_week_and_day
day_of_year days_to_go
days_in_month
UK_tax_week UK_tax_month
working_days
today now
J2G
v_date r_date
adjust_to_local_time adjust_to_UTC
is_a_date
);
=pod
You can import all of them at once with
C<use Cal::Date ':all';>
=cut
%EXPORT_TAGS = (all => [@EXPORT_OK]);
=over 4
=item MJD(yyyymmdd) or MJD(y,m,d)
MJD returns the `modified julian day' number for a date.
This is suitably small integer that you can use as the basis of many
date calculations. You can call C<MJD()> with a single 8 digit string
representing a date in compact ISO form, C<yyyymmdd>, or with three integers
representing year, month and day of the month.
Unlike the values returned from the C<gmtime()> etc. functions,
year is the full AD year and month 1 is January. Other than checking
that the arguments are whole numbers, the internal function C<_getYMD>
does no range checking. This is a feature rather than a bug. It means
you can use 0 as a month number to refer to December in the previous year,
and 13 to refer to January in the next year. For example,
assuming C<$month == 12>, the following are equivalent:
MJD(19991301);
MJD(1999, $month+1, 1);
MJD(20000101);
MJD(2000, 1, 1);
You can do the same trick with the day numbers too; this provides a handy
way to refer to the last day of the previous month. Thus C<MJD(20000100)>
refers to 31 December 1999 (but note that C<MJD(20000000)> refers to
30 November 1999). This works with leap years too (of course) so
C<MJD($y,3,0)> refers to the last day of February for any value of C<$y>.
=cut
sub MJD { # returns mjd from yyyymmdd or y,m,d
use integer;
my ($y, $m, $d) = &_getYMD;
# allow month to be enormous
while ( $m > 12) { $m -= 12; $y++ }
# adjust the month/year to make it a date after 1 March
if ($m < 3) { $m += 12; $y-- }
# work out days upto and including the day before the previous 1 March
# year * 365 + leap days - 306
# we are using the (possibly proleptic) Gregorian calendar
my $mjd = $y*365 + $y/4 - $y/100 + $y/400 - 306;
# add days since previous 1 March (incl)
$mjd += ($m+1)*306/10 - 122 + $d;
# adjust so 0 == 18 Nov 1858 == JD 2,400,000.5
$mjd -= 678576;
return $mjd;
}
=item DJM(mjd)
This function is the inverse of the C<MJD()> function, hence the rather
cute name. It takes any number, interprets it as an MJD number and returns
the corresponding date in the ISO compact form of YYYYMMDD. This form has
the advantage of being easily sorted and compared.
C<DJM()> is often used in combination with MJD. For example to `correct'
a date use C<DJM(MJD(yyyymmdd))>. If your input date was 20000300, this will
return 20000229. This idiom can also be used to check that an input date is
valid. Like this:
if ($date ne DJM(MJD($date)) ) {
print "$date is not a valid YYYYMMDD date\n";
}
When you pass a real number to C<DJM()> the fractional part is interpreted
as a fraction of a day, and the date and time are returned in C<YYYYMMDD HH:MM>
form. Like this:
print DJM(51455.7356) . "\n"; # prints 19991004 17:39
If you call C<DJM()> in a list context then the parts of the date/time
are returned as elements of a list, like this:
($y, $m, $d, $hr, $min) = DJM(51455.7356);
($y, $m, $d) = DJM(51500);
=cut
sub DJM { # returns yyyymmdd from mjd
return unless defined wantarray; # don't bother doing more
# the supplied MJD may be integer (hour=midnight) or real
# the fractional part repesents the time of day
my $mjd = shift;
# convert to full Julian number
my $jd = $mjd + 2400000.5;
# jd0 is the Julian number for noon on the day in question
# for example mjd jd jd0 === mjd0
# 3.0 ...3.5 ...4.0 === 3.5
# 3.3 ...3.8 ...4.0 === 3.5
# 3.7 ...4.2 ...4.0 === 3.5
# 3.9 ...4.4 ...4.0 === 3.5
# 4.0 ...4.5 ...5.0 === 4.5
my $jd0 = int($jd+0.5);
# next we convert to Julian dates to make the rest of the maths easier.
# JD1867217 = 1 Mar 400, so $b is the number of complete Gregorian
# centuries since then. The constant 36524.25 is the number of days
# in a Gregorian century. The 0.25 on the other constant ensures that
# $b correctly rounds down on the last day of the 400 year cycle.
# For example $b == 15.9999... on 2000 Feb 29 not 16.00000.
my $b = int(($jd0-1867216.25)/36524.25);
# b-int(b/4) is the number of Julian leap days that are not counted in
# the Gregorian calendar, and 1402 is the number of days from 1 Jan 4713BC
# back to 1 Mar 4716BC. $c represents the date in the Julian calendar
# corrected back to the start of a leap year cycle.
my $c = $jd0+($b-int($b/4))+1402;
# d is the whole number of Julian years from 1 Mar 4716BC to the date
# we are trying to find.
my $d = int(($c+0.9)/365.25);
# e is the number of days from 1 Mar 4716BC to 1 Mar this year
# using the Julian calendar
my $e = 365*$d+int($d/4);
# c-e is now the remaining days in this year from 1 Mar to our date
# and we need to work out the magic number f such that f-1 == month
my $f = int(($c-$e+123)/30.6001);
# int(f*30.6001) is the day of the start of the month
# so the day of the month is the difference between that and c-e+123
my $day = $c-$e+123-int(30.6001*$f);
# month is now f-1, except that Jan and Feb are f-13
# ie f 4 5 6 7 8 9 10 11 12 13 14 15
# m 3 4 5 6 7 8 9 10 11 12 1 2
my $month = ($f-2)%12+1;
# year is d - 4716 (adjusted for Jan and Feb again)
my $year = $d - 4716 + ($month<3);
# finally work out the hour (if any)
my $hour = 24 * ($jd+0.5-$jd0);
if ( $hour == 0) {
if (wantarray) {
return ($year, $month, $day)
}
else {
return sprintf "%d%02d%02d", ($year, $month, $day)
}
}
else {
$hour = int($hour*60+0.5)/60; # round to nearest minute
my $min = int(0.5+60 * ($hour - int($hour)));
$hour = int($hour);
if (wantarray) {
return $year, $month, $day, $hour, $min
}
else {
return sprintf "%d%02d%02d %02d:%02d", $year, $month, $day, $hour, $min
}
}
}
=item today() or today(delta)
This function returns today's date in YYYYMMDD form, saving you
all that tedious mucking about with lists and C<undef>s.
It uses C<localtime()> so you get the date adjusted for local time
zone, depending on the time of day this may or may not be the same
as the date at Greenwich. Use C<adjust_to_UTC> to get the UTC date if
that's what you want.
You can supply a number of days as an optional parameter. This number (which
may be negative) will be added to the current date. The number should be a
either a whole number of days or a week specification in a form that will
match C</^[+-]?\d+[wW]\d?$/>. For example: C<1w> means one week, C<-2w3>
means -17 days.
=cut
sub today { # return YYYYMMDD for today
return unless defined wantarray;
my $delta = &_get_delta;
return DJM(MJD()+$delta);
}
sub _get_delta {
my $delta = shift || 0;
if ($delta =~ /^([+-])?(\d+)[wW](\d)?$/) {
local $^W=0; # disable warnings for unitialized $1 or $3
$delta = $1.($2*7+$3)
}
if ( $delta !~ /^([+-]?\d+)$/ ) {
croak "Bad value for day shift: $delta\n";
}
return $delta;
}
sub now { # return hh:mm for now
return unless defined wantarray;
my ($s, $m, $h) = localtime();
return wantarray ? ($h,$m,$s) : sprintf("%02d:%02d:%02d", $h, $m, $s);
}
=item Easter(year,[delta])
This function takes a year number and returns the date of Easter Sunday
in YYYYMMDD form for that year. See below about valid years. The date
is supposed to be the first Sunday after the calendar full moon which
occurs on or after 21 March. The name Easter comes from the Saxon
goddess of the dawn, Eostre, whose festival was celebrated at the vernal
equinox.
You can supply a number of days as an optional parameter. This number
(which may be negative) will be added to the resulting date. This is
handy for working out dates that depend on Easter. For example:
$y = 2000;
$s = Easter($y,-47); # Shrove Tuesday (Pancake Day)
$m = Easter($y,-21); # Mothers day in the UK
$a = Easter($y,+39); # Ascension day
The format of the number should be as described above under L<today()>.
The algorithm used was adapted from D. E. Knuth I<Fundamental
Algorithms>, as Knuth notes it is derived from older sources, and is
only valid after 1582 when the Gregorian calendar was first used in
Europe (but not in Britain). For years before this use the
L<old_style_Easter()> routine below, which returns Julian dates such as
were in use then. I have only validated this routine back to 1066, the
earliest I could find a list in my reference books at home, but it
should be valid further back. I do not know when Easter was first
celebrated as Easter.
=cut
sub Easter {
return unless defined wantarray; # don't bother doing more
use integer;
my $y = shift;
my $delta = &_get_delta;
my $golden = $y%19 + 1;
my $century = $y/100 + 1;
my $x = 3*$century/4 - 12;
my $q = 5*$y/4 - $x - 10;
my $epact = (11*$golden + 15 + (8*$century + 5)/25 - $x) % 30;
++$epact if ($epact == 25 && $golden > 11) || $epact == 24;
my $d = 44 - $epact;
$d += 30 if $d < 21;
$d = $d + 7 - (($q+$d)%7);
return DJM(MJD($y,3,$d)+$delta);
}
=item old_style_Easter(year,[delta])
This function is mainly of historical interest. Before the switch to
Gregorian dates that happened in 1582 in certain parts of Roman Catholic
Europe, the Julian calendar was used. This routine gives you the date
of Easter in the Julian calendar. Because of the way Easter is derived,
this is not a constant number of days apart from the date in Gregorian.
Typically it can be either 4 or 5 weeks or just a few days.
In British historical records between 1582 and 1752 (when Britain
switched) the Julian dates are referred to as `old style' and the
Gregorian dates as `new style'. Hence my name for this function. This
algorithm is based on details found on the web which referred to the
algorithm of Oudin (1940), quoted in I<Explanatory Supplement to the
Astronomical Almanac>, P. Kenneth Seidelmann, editor.
You can add an optional day shift number as above in L<Easter()>.
=cut
sub old_style_Easter {
return unless defined wantarray; # don't bother doing more
use integer;
my $y = shift;
my $delta = &_get_delta;
my $g = $y % 19;
my $i = (19*$g + 15) % 30;
my $j = ($y + $y/4 + $i) % 7;
my $l = $i - $j;
my $m = 3 + ($l + 40)/44;
my $d = $l + 28 - 31*($m/4);
return DJM(MJD($y,$m,$d)+$delta);
}
=item orthodox_Easter(year,[delta])
The various Orthodox parts of the Christian church (principally in Greece, the
Balkans and other parts of eastern Europe and Russia) still use the Julian calendar
(the `old style') to work out the date of Easter, but they express the result
in new style, Gregorian dates. This routine may be handy if you belong to such
a church or if you are planning a spring holiday in Greece, where Easter is always
a special time.
This is essentially just old_style_Easter corrected to Gregorian dates with the L<J2G()> function.
=cut
sub orthodox_Easter {
my ($y,$m,$d) = &old_style_Easter;
return DJM(MJD($y,$m,$d)+J2G($y,$m,$d));
}
=item ISO_week(yyyymmdd) or ISO_week(y,m,d)
This function returns the week number according to the ISO standard.
This states that weeks begin on a Monday (day 1), and that the first
week of a year is the one with 4 Jan in it. The function returns the
date in the ISO week form: yyyy-Wnn. The year is included as it may
differ from the year of the date in yyyymmdd form. For example
C<ISO_week(20000101)> returns C<1999-W52>.
The ISO day number for a given date is given by C<ISO_day()>. See below.
=cut
sub ISO_week {
return unless defined wantarray; # don't bother doing more
use integer;
my ($y, $m, $d) = &_getYMD;
my $jan1 = MJD($y,1,1);
my $week = (MJD($y,$m,$d) - $jan1 + 1 + ($jan1+5)%7 + 3) / 7;
if ( $week == 0 ) {
# week belongs to last year
$y--;
# work out if its W52 or W53
$jan1 = MJD($y,1,1);
$week = (MJD($y,12,31) - $jan1 + 1 + ($jan1+5)%7 + 3) / 7;
}
elsif ( $week == 53 ) {
# week might belong to next year
# if 31 Dec is Weds or earlier
if (ISO_day(MJD($y,12,31)) < 4) {
$y++;
$week = 1;
}
}
return wantarray ? ($y, $week) : sprintf "%d-W%02d", $y, $week;
}
=item ISO_day(mjd)
This function returns the ISO day number for a given MJD value.
According to ISO, Monday is day 1 and Sunday day 7 in the week.
To find today's ISO day number do:
print ISO_day(MJD(today()));
I occasionally find that I call this with a date by mistake for an MJD
number, so as a convenience if the MJD number is over 10,000,000 we will
interpret it as a date. This means that ISO_day won't work for dates
after 29237-12-12, which we can probably live with, but that
c<ISO_day(20010117)> gives a less astonishing result.
=cut
sub ISO_day {
my $mjd = shift;
if ($mjd > 10_000_000) {
$mjd = MJD($mjd);
}
if ($mjd > -3) {
return ($mjd+2)%7+1;
}
else {
return abs(9+$mjd%7)%7+1;
}
}
=item ISO_week_and_day(yyyymmdd) or ISO_week_and_day(y,m,d)
Converts a given date to ISO Week.Day form, sometimes known as business
date form. For example 19991215 maps to 1999-W51-6
=cut
sub ISO_week_and_day {
return unless defined wantarray; # don't bother doing more
return wantarray ? (&ISO_week, ISO_day(&MJD)) : &ISO_week . '-' . ISO_day(&MJD)
}
=item day_of_year(yyyymmdd) or day_of_year(y,m,d)
This function returns the day number of the current year, where Jan 1 = 1,
Feb 1 = 32 etc. It is implemented simply as
MJD($y,$m,$d) - MJD($y-1,12,31)
=cut
sub day_of_year {
my ($y, $m, $d) = &_getYMD;
return MJD($y,$m,$d)-MJD($y,1,0);
}
=item days_to_go(yyyymmdd) or days_to_go(y,m,d)
This function returns the days to the end of the year, where Dec 31 = 0,
Dec 30 = 1, etc. Again it is simply implemented as
MJD($y,12,31)-MJD($y,$m,$d);
=cut
sub days_to_go {
my ($y, $m, $d) = &_getYMD;
return MJD($y,12,31)-MJD($y,$m,$d);
}
=item days_in_month(y,m)
This function returns the days in the current month. It is implemented
like this:
MJD($y,$m+1,1)-MJD($y,$m,1);
Note that this works even in December (when C<$m==12>)
because C<MJD()> interprets 13 to mean January next year.
You may find it easier to use MJD directly for this function, and save
an import.
=cut
sub days_in_month {
my ($y, $m) = @_;
return MJD($y,$m+1,1)-MJD($y,$m,1);
}
=item UK_tax_week(yyyymmdd) or UK_tax_week(y,m,d)
This function is specific to UK Income Tax or `Pay As You Earn' rules.
It returns a string indicating the week in the tax year corresponding to a
given date. The UK tax year starts on April 5 each year. Example:
print UK_tax_week(19991225); # Prints: PAYE Week 38
=cut
sub UK_tax_week {
my ($y, $m, $d) = &_getYMD;
my $april6 = MJD($y,4,6);
my $today = MJD($y,$m,$d);
if ($april6 > $today ) { $april6 = MJD($y-1,4,6) }
use integer;
return sprintf "%d", ($today-$april6)/7+1;
}
=item UK_tax_month()
This function is also specific to UK Income Tax or `Pay As You Earn' rules.
It returns a string indicating the month in the tax year corresponding to a
given date. The UK tax year starts on April 5 each year. Example:
print UK_tax_month(19991225); # Prints: PAYE Month 9
=cut
sub UK_tax_month {
my ($y, $m, $d) = &_getYMD;
return sprintf "%d", ($m+8-($d<6))%12+1;
}
=item working_days(y,m,d,period) or working_days(y,m,d,y2,m2,d2)
This function returns the number of working days in a given period including
start day. Call it with a date and a number of days or with two dates. The
number of days returned is simply the number of non-weekend days, no account
is taken of holidays etc. More sophisticated functions can be found in the
C<Date::Manip> package. The two dates can be given in either order. Should
they be the same, then 1 or 0 may be returned depending on whether the day in
question was a working day or not.
=cut
sub working_days {
my ($start,$end,$m,$count);
$start = MJD($_[0],$_[1],$_[2]);
if (@_ == 4) { $end = $start + $_[3] - 1; }
elsif (@_ == 6) { $end = MJD($_[3],$_[4],$_[5]); }
else { croak "Bad call to working days: $!\n" }
if ($start > $end ) { ($start,$end) = ($end,$start)}
if ($end-$start > 10000 ) { return 'Lots' }
$count = 0;
for $m ($start..$end) {
++$count if ISO_day($m) < 6
}
return $count;
}
=item v_date(year,datespec[,delta])
v_date returns a date as a real MJD (or (y,m,d,h,min,s) in list context)
optionally shifted by delta days, based on the specification in datespec
and the given year.
The format of the delta number should be as described above under L<today()>.
This specification can be one of the standard variable date forms used in
setting a Posix TZ environment variable, extended as noted here.
The main form is Mmm.w.d where `mm' is the month (1-12) number, `w' is the
week of the month (1-5 or L) note that 5 and L are equivalent and refer to
the last week of the months (either the fourth or fifth depending on the
length of the month), and `d' is the day of the week (0-7) where 1 = Monday
and 7 (or 0) = Sunday.
The use of L and 7 above are extensions to the Posix rules. Further you can
extend the meaning of `d' to allow you to specify for example the last working
day in a month. You do this by adding to the d number, eg:
M10.L.12345 means the last working day of October, while
M1.1.67 means the first weekend day in January.
Other forms are...
- Jddd which refers to the day of the year, regardless of leap days (ie 1
March is always day J60 etc).
- ddd which refers to the day of the year counting leap days, (ie day 60 is
Feb 29 in leap years or Mar 1 in non-leap years.
- Dmm.d.w which is exactly the same as the M form, but with the w and d
fields reversed.
Any of the specs may be followed by "/hh[:mm[:ss]]" to indicate a particular
time.
v_date returns undef if called with an invalid spec.
=cut
sub v_date {
return unless defined wantarray;
my $y = shift;
my $spec = shift;
my $delta = &_get_delta;
my ($m,$w,$d,$mjd,$time,$dshift);
# remove any time from spec
if ( $spec =~ /(.*)\/(\d+)(:(\d+)(:(\d+))?)?/ ) {
$time = $2;
if ( defined($4) ) {
$time += $4/60;
if ( defined($6) ) {
$time += $6/3600;
}
}
$spec = $1;
}
else { $time = 0 }
# change D.... to M....
if (($m,$d,$w) = $spec =~ /^D([0-1]?\d).([0-7]+).([1-5L])$/ ) {
$spec = "M$m.$w.$d";
}
# Mmm.w.d
if (($m,$w,$d) = $spec =~ /^M([0-1]?\d).([1-5L]).([0-7]+)$/ ) {
if ($w =~ /[1-4]/ ) {
$mjd = MJD($y,$m,1) + 7*($w-1);
$dshift = 7;
for my $n ( split(/ */,$d)) {
$n = $n - ISO_day($mjd);
if ($n<0) { $n += 7 }
if ($n<$dshift) { $dshift = $n }
}
}
else { # 5 or L
$mjd = MJD($y,$m+1,0);
$dshift = 7;
for my $n ( split(/ */,$d) ) {
$n = $n - ISO_day($mjd)%7;
if ($n>0) { $n -= 7 }
if (abs($n)<abs($dshift)) { $dshift = $n }
}
}
$mjd = $mjd+$dshift+$delta;
}
# Jnnn ....
elsif (($d) = $spec =~ /^J(\d+)$/ ) {
if ($d>59) { $mjd = MJD($y,3,1)+$d-60+$delta }
else { $mjd = MJD($y,1,0)+$d+$delta }
}
# nnn ...
elsif (($d) = $spec =~ /^(\d+)$/ ) {
$mjd = MJD($y,1,0)+$d+$delta
}
else {
croak "Malformed spec for v_date: $spec\n";
}
$mjd += $time/24;
return wantarray ? DJM($mjd) : $mjd;
}
=item r_date(dow[,every[,start[,end]]])
This routine generates a list of MJD integers corresponding to a set of
repeating dates defined by the argument list. The set may be empty in which
case an empty list is returned. In the scalar context you get the number of
dates in the list. The list is returned sorted in ascending numerical order.
dow: should match C</\d/ & /^1?2?3?4?5?6?7?$/>, that is at least one and
at most seven digits between 1 and 7 with no repetitions. So "1" means
Mondays, "6" means Saturdays, "14" means Mondays and Thursdays and so on.
every: 1 means every dow, 2 means every other dow, 3 means every third dow, etc.
Every defaults to 1.
start: is a date in yyyymmdd form. The first date in the returned list
will be on or after this date. Start defaults to Jan 1st in the current year.
end: is another date in yyyymmdd form. The last date in the returned list
will be on or before this date. End defaults to Dec 31st in the current year.
Some examples:
r_date(1) returns a list of every Monday in the current year
r_date(2,2,20030101,20030700)
returns every other Tuesday in the first half of 2003
r_date(15,1,20030501,20030531)
every Monday and Friday in June 2003
=cut
sub r_date {
return unless defined wantarray;
my (undef,undef,undef,undef,undef,$y) = localtime;
my $days = shift;
my $every = shift;
my $start = shift;
my $end = shift;
return undef unless defined $days && $days =~ /\d+/ && $days =~ /^1?2?3?4?5?6?7?$/;
$every = 1 unless defined $every && $every =~ /^\d+$/ && $every<100;
if ( defined $start && $start=~/^\d{8}$/ ) { $start = MJD($start) }
else { $start = MJD($y,1,1) }
if ( defined $end && $end =~/^\d{8}$/ ) { $end = MJD($end) }
else { $end = MJD($y,12,31) }
my @list = ();
for my $dow ( split / */, $days) {
my $day_shift = $dow - ISO_day($start);
$day_shift += 7 if $day_shift < 0;
my $first_date = $start + $day_shift;
for (my $i=0; $first_date+$i<$end; $i+=7*$every) {
push @list, $first_date+$i;
}
}
return sort @list;
}
=item adjust_to_local_time(mjd,tzoffset,tzrule1,tzrule2[,DST_delta])
This routine takes a real MJD number --- representing a UTC date and time ---
and adjusts it for time zone making proper allowance for summer time or
`daylight saving time' (DST). The second argument is the normal difference
between UTC and local time (ie New York = +5) in hours.
The third and fourth arguments are two rules that define when DST should
start when it should stop. If the rules are empty or undefined then the
routine returns the MJD adjusted to local time with no allowance for summer
time. The rules are rules in the format understood by C<v_date()>.
The fifth argument represents the number of hours that the clocks go forward
when DST starts. If this is omitted it will default to 1. This default was
not always correct historically but as far as I have been able to verify it
is currently, so you can nearly always omit the fifth argument.
=cut
sub adjust_to_local_time {
my $mjd = shift;
my $tz = shift || $Cal::Astro::tz;
my $r1 = shift || $Cal::Astro::r1;
my $r2 = shift || $Cal::Astro::r2;
my $dst_delta = shift || 1;
# stop here if no date given
return '' unless defined($mjd);
return '' if $mjd eq '';
# stop here if no TZ given
return $mjd unless defined($tz);
# adjust for time zone
$mjd = $mjd-$tz/24;
# stop here if no summer time rules
return $mjd unless defined($r1) && defined($r2);
# make rules into dates for the current year
my ($year) = DJM($mjd);
my $d1 = v_date($year,$r1);
my $d2 = v_date($year,$r2);
# are we in DST at the start of the year?
# (ie does r1 say October rather than March/April)
my $jan_state = ($d1 > $d2);
# swap the dates so that d1 < d2
($d1,$d2) = ($d2,$d1) if $jan_state;
# if the date is in the summer set the opposite of
# the state at the start of the year & adjust if needed
if ($d1 <= $mjd && $mjd < $d2 ) {
return $mjd + $dst_delta/24 * !$jan_state;
}
# otherwise return the state at the start of the year
return $mjd + $dst_delta/24 * $jan_state;
}
=item adjust_to_UTC(mjd,tzoffset,tzrule1,tzrule2[,DST_delta])
This routine takes a real MJD number --- representing a local date and time ---
and adjusts it back to UTC allowing for local time zone and
summer time rules.
The arguments are all exactly the same as those for C<adjust_to_local_time()>.
=cut
sub adjust_to_UTC {
my $mjd = shift;
my $tz = shift || $Cal::Astro::tz;
my $r1 = shift || $Cal::Astro::r1;
my $r2 = shift || $Cal::Astro::r2;
my $dst_delta = shift || 1;
return adjust_to_local_time($mjd,-$tz,$r1,$r2,-$dst_delta);
}
sub is_a_date {
my $date = shift;
$date =~ s/[^0-9]//g;
return 0 unless $date =~ /\d{8}/;
return $date eq DJM(MJD($date));
}
sub _getYMD {
my ($y, $m, $d);
if ( @_ == 0 ) {
(undef, undef, undef, $d, $m, $y) = localtime();
$y += 1900;
$m ++;
} elsif ( @_ == 1 && !defined $_[0] ) {
my ($package, $filename, $line) = caller;
croak "\nCal::Date routine called with undefined value by $package \nLook at $filename, line $line\n";
} elsif ( @_ == 1 && $_[0] =~ /^\d+$/ && $_[0] > 100000 ) {
$y = substr($_[0],0,-4);
$m = substr($_[0],-4,2);
$d = substr($_[0],-2);
} elsif ( @_ == 1 && $_[0] =~ /^\d+$/ ) {
# probably an MJD as it is so small
($y, $m, $d) = DJM($_[0]);
} elsif (@_ == 1 && $_[0] =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/ ) {
($y, $m, $d) = ($1, $2, $3);
} elsif ( @_ == 3
&& $_[0] =~ /^\d+$/
&& $_[1] =~ /^[-]?\d+$/
&& $_[2] =~ /^[-]?\d+$/) {
($y, $m, $d) = @_
} else {
croak "Can't read a date from this --> [@_]"
}
return ($y, $m, $d);
}
sub J2G { # returns days difference between julian on gregorian dates
use integer;
my ($y, $m, $d) = &_getYMD;
# if the month is Jan or Feb then use the year before
if ($m < 3) { $y-- }
# the difference in leap days is just the omitted century end leap days in the
# Gregorian calendar, less two because they didn't start until
# some long time after 1 AD
return $y/100 - $y/400 - 2;
}
=back
=head1 SEE ALSO
L<Date::Calc> and L<Date::Manip> packages which provide more comprehensive
functions; as they say: there's more than one way to do it.
=head1 AUTHOR
Toby Thurston
web: http://www.wildfire.dircon.co.uk
=cut
1;