File Coverage

blib/lib/Advanced/Config/Date.pm
Criterion Covered Total %
statement 564 856 65.8
branch 220 448 49.1
condition 137 396 34.6
subroutine 36 44 81.8
pod 14 16 87.5
total 971 1760 55.1


line stmt bran cond sub pod time code
1             ###
2             ### Copyright (c) 2018 - 2026 Curtis Leach. All rights reserved.
3             ###
4             ### Module: Advanced::Config::Date
5              
6             =head1 NAME
7              
8             Advanced::Config::Date - Module for parsing dates for L.
9              
10             =head1 SYNOPSIS
11              
12             use Advanced::Config::Date;
13             or
14             require Advanced::Config::Date;
15              
16             =head1 DESCRIPTION
17              
18             F is a helper module to L. So it
19             should be very rare to directly call any methods defined by this module. But
20             it's perfectly OK to use this module directly if you wish.
21              
22             It's main job is to handle parsing dates passed in various formats and languages
23             while returning it in the standardized format of: S. Hiding all the
24             messy logic of how to interpret any given date string.
25              
26             =head1 MULTI-LANGUAGE SUPPORT
27              
28             By default this module only supports parsing B language dates.
29              
30             But if you have the I and/or I modules installed
31             you can ask for it to use another language supported by either of these modules
32             instead.
33              
34             You have to explicitly allow languages that require the use of I.
35             Otherwise they are not supported.
36              
37             If a language is defined in both modules, it will merge the data together.
38             Since both modules sometimes give extra information that can be useful in
39             parsing a date..
40              
41             =head1 FOUR-DIGIT VS TWO-DIGIT YEARS IN A DATE
42              
43             This module will accept both 4-digit and 2-digit years in the dates it parses.
44             But two-digit years are inherently ambiguous if you aren't given the expected
45             format up front. So 2-digit years generate more unreliability in the parsing
46             of any dates by this module.
47              
48             So when used by the L module, that module gives you the
49             ability to turn two-digit years on or off. This is done via the B
50             "B" which defaults to 0, B allowing two-digit years.
51              
52             To help resolve ambiguity with numeric dates, there is an option "B"
53             that tells the L how to parse these dates. See the order
54             argument for I and I for how this
55             is done.
56              
57             Finally if you use "B" and module L is
58             installed, it will enhance parse_date() with that module's str2time() parser.
59             So if this option was used, it doesn't make much sense to disable 2-digit years.
60             Since we can't turn off 2-digit year support for str2time().
61              
62             See L for more options telling how that module
63             controls how L uses this module for parsing dates.
64              
65             Those options have no effect if you are calling these methods directly.
66              
67             =head1 FUNCTIONS
68              
69             =over 4
70              
71             =cut
72              
73             package Advanced::Config::Date;
74              
75 28     28   185141 use strict;
  28         45  
  28         1083  
76 28     28   152 use warnings;
  28         48  
  28         1357  
77              
78 28     28   174 use File::Spec;
  28         46  
  28         854  
79 28     28   221 use File::Glob qw (bsd_glob);
  28         79  
  28         3257  
80              
81 28     28   189 use vars qw( @ISA @EXPORT @EXPORT_OK $VERSION );
  28         91  
  28         1914  
82 28     28   137 use Exporter;
  28         82  
  28         1741  
83              
84 28     28   219 use Fred::Fish::DBUG 2.09 qw / on_if_set ADVANCED_CONFIG_FISH /;
  28         552  
  28         244  
85             $VERSION = "1.14";
86             @ISA = qw( Exporter );
87              
88             @EXPORT = qw( get_languages
89             swap_language
90             parse_date
91             adjust_future_cutoff
92             make_it_a_4_digit_year
93             parse_8_digit_date
94             parse_6_digit_date
95             init_special_date_arrays
96             _date_language_installed
97             _date_manip_installed
98             _validate_date_str
99             is_leap_year
100             calc_hundred_year_date
101             calc_day_of_week
102             convert_hyd_to_date_str
103             calc_day_of_year
104             adjust_date_str
105             );
106              
107             @EXPORT_OK = qw( );
108              
109             my $global_cutoff_date = 30; # Defaults to 30 years in the future ...
110              
111             # Thesee haahes tell which language modules are available ...
112             my %date_language_installed_languages;
113             my %date_manip_installed_languages;
114              
115             # ========================================================================
116             # Detects if the optional Date::Language module is available ...
117             # If it's not installed, you'll be unable to swap languages using it!
118             BEGIN
119             {
120 28     28   17720 eval {
121 28         236 local $SIG{__DIE__} = "";
122 28         13384 require Date::Language;
123              
124             # Find out where it's installed
125 28         323757 my $loc = $INC{"Date/Language.pm"};
126 28         211 $loc =~ s/[.]pm$//;
127              
128 28         492 my $search = File::Spec->catfile ($loc, "*.pm");
129              
130             # Get's the list of languages supported.
131 28         17595 foreach my $f ( bsd_glob ($search) ) {
132 980         3895 my $module = (File::Spec->splitdir( $f ))[-1];
133 980         3002 $module =~ s/[.]pm$//;
134              
135 980         2582 my %data = ( Language => $module,
136             Module => "Date::Language::${module}" );
137 980         8017 $date_language_installed_languages{lc($module)} = \%data;
138             }
139             };
140             }
141              
142             # ========================================================================
143             # Detects if the optional Date::Manip module is available ...
144             # If it's not installed, you'll be unable to swap languages using it!
145             BEGIN
146             {
147 28     28   125 eval {
148 28         164 local $SIG{__DIE__} = "";
149 28         20105 require Date::Manip::Lang::index;
150 0         0 Date::Manip::Lang::index->import ();
151              
152 0         0 foreach my $k ( sort keys %Date::Manip::Lang::index::Lang ) {
153 0         0 my $mod = $Date::Manip::Lang::index::Lang{$k};
154 0 0       0 my $lang = ( $k eq $mod ) ? ucfirst ($mod) : $mod;
155 0         0 my $module = "Date::Manip::Lang::${mod}";
156              
157 0         0 my %data = ( Language => $lang, # A guess that's wrong sometimes
158             Module => $module );
159 0         0 $date_manip_installed_languages{lc ($k)} = \%data;
160             }
161             };
162              
163             # -------------------------------------------------------------
164             # Proves sometimes the module name is different from the
165             # real language name.
166             # -------------------------------------------------------------
167             # foreach my $k ( sort keys %date_manip_installed_languages ) {
168             # printf STDERR ("Key (%s) Language (%s)\n", $k, $date_manip_installed_languages{$k}->{Language});
169             # }
170             }
171              
172             # ========================================================================
173             # Hashes used to help validate/parse dates with ...
174             # Always keep the keys in lower case.
175              
176             # Using the values from Date::Language::English for initialization ...
177             # Hard coded here in case Date::Language wasn't installed ...
178              
179             # These hashes get rebuilt each time swap_language() is
180             # successfully called!
181             # ========================================================================
182             # Used by parse_date ();
183              
184             my %last_language_edit_flags;
185              
186             # Variants for the month names & days of month ...
187             # We hard code the initialization in case neither
188             # language module is installed locally.
189             my %Months;
190             my %Days;
191              
192             BEGIN {
193             # Variants for the month names ...
194 28     28   988 %Months = (
195             # The USA Months spelled out ...
196             # Built from the @Date::Language::English::MoY array ...
197             "january" => 1, "february" => 2, "march" => 3,
198             "april" => 4, "may" => 5, "june" => 6,
199             "july" => 7, "august" => 8, "september" => 9,
200             "october" => 10, "november" => 11, "december" => 12,
201              
202             # The USA Months using 3 char abreviations ("may" not repeated!)
203             # Built from the @Date::Language::English::MoYs array ...
204             "jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4,
205             "jun" => 6, "jul" => 7, "aug" => 8,
206             "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12,
207              
208             # Months as a numeric value. If all digits, leading zeros will
209             # be removed before it's used as a key.
210             "1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5, "6" => 6,
211             "7" => 7, "8" => 8, "9" => 9, "10" => 10, "11" => 11, "12" => 12
212             );
213              
214             # variants for days of the month ...
215 28         1986 %Days = (
216             "1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5,
217             "6" => 6, "7" => 7, "8" => 8, "9" => 9, "10" => 10,
218             "11" => 11, "12" => 12, "13" => 13, "14" => 14, "15" => 15,
219             "16" => 16, "17" => 17, "18" => 18, "19" => 19, "20" => 20,
220             "21" => 21, "22" => 22, "23" => 23, "24" => 24, "25" => 25,
221             "26" => 26, "27" => 27, "28" => 28, "29" => 29, "30" => 30,
222             "31" => 31,
223              
224             # Built from the optional @Date::Language::English::Dsuf array ...
225             "1st" => 1, "2nd" => 2, "3rd" => 3, "4th" => 4, "5th" => 5,
226             "6th" => 6, "7th" => 7, "8th" => 8, "9th" => 9, "10th" => 10,
227             "11th" => 11, "12th" => 12, "13th" => 13, "14th" => 14, "15th" => 15,
228             "16th" => 16, "17th" => 17, "18th" => 18, "19th" => 19, "20th" => 20,
229             "21st" => 21, "22nd" => 22, "23rd" => 23, "24th" => 24, "25th" => 25,
230             "26th" => 26, "27th" => 27, "28th" => 28, "29th" => 29, "30th" => 30,
231             "31st" => 31,
232              
233             # From Date::Manip::Lang::english::Language->{nth} arrays ...
234             'first' => -1, 'second' => -2, 'third' => -3,
235             'fourth' => -4, 'fifth' => -5, 'sixth' => -6,
236             'seventh' => -7, 'eighth' => -8, 'ninth' => -9,
237             'tenth' => -10, 'eleventh' => -11, 'twelfth' => -12,
238             'thirteenth' => -13, 'fourteenth' => -14, 'fifteenth' => -15,
239             'sixteenth' => -16, 'seventeenth' => -17, 'eighteenth' => -18,
240             'nineteenth' => -19, 'twentieth' => -20, 'twenty-first' => -21,
241             'twenty-second' => -22, 'twenty-third' => -23, 'twenty-fourth' => -24,
242             'twenty-fifth' => -25, 'twenty-sixth' => -26, 'twenty-seventh' => -27,
243             'twenty-eighth' => -28, 'twenty-ninth' => -29, 'thirtieth' => -30,
244             'thirty-first' => -31,
245              
246             # From Date::Manip::Lang::english::Language->{nth} arrays ...
247             'one' => -1, 'two' => -2, 'three' => -3,
248             'four' => -4, 'five' => -5, 'six' => -6,
249             'seven' => -7, 'eight' => -8, 'nine' => -9,
250             'ten' => -10, 'eleven' => -11, 'twelve' => -12,
251             'thirteen' => -13, 'fourteen' => -14, 'fifteen' => -15,
252             'sixteen' => -16, 'seventeen' => -17, 'eighteen' => -18,
253             'nineteen' => -19, 'twenty' => -20, 'twenty-one' => -21,
254             'twenty-two' => -22, 'twenty-three' => -23, 'twenty-four' => -24,
255             'twenty-five' => -25, 'twenty-six' => -26, 'twenty-seven' => -27,
256             'twenty-eight' => -28, 'twenty-nine' => -29, 'thirty' => -30,
257             'thirty-one' => -31,
258             );
259              
260 28         104 my $date_manip_installed_flag = keys %date_manip_installed_languages;
261 28         205 my $date_language_installed_flag = keys %date_language_installed_languages;
262              
263             # Tells what to do about the negative values in the hashes ...
264 28   33     274 my $flip = $date_manip_installed_flag || (! $date_language_installed_flag);
265              
266              
267 28         106 $last_language_edit_flags{language} = "English";
268              
269 28         88 $last_language_edit_flags{month_period} = 0;;
270 28         155 $last_language_edit_flags{dsuf_period} = 0;
271 28         75 $last_language_edit_flags{dow_period} = 0;;
272              
273 28         340 foreach ( keys %Months ) {
274 980 50       1749 next if ( $Months{$_} > 0 );
275 0 0       0 if ( $flip ) {
276 0         0 $Months{$_} = abs ($Months{$_});
277             } else {
278 0         0 delete $Months{$_};
279             }
280             }
281              
282 28         657 foreach ( keys %Days ) {
283 3472 100       237974 next if ( $Days{$_} > 0 );
284 1736 50       3100 if ( $flip ) {
285 0         0 $Days{$_} = abs ($Days{$_});
286             } else {
287 1736         166179 delete $Days{$_};
288             }
289             }
290             }
291              
292             # How many days per month ... (non-leap year)
293             # ---------------------> J F M A M J J A S O N D
294             my @days_in_months = ( 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
295              
296              
297             # Updated by: init_special_date_arrays() ...
298             # May be for a different language than the above hashes ...
299             my $prev_array_lang = "English";
300             my @gMoY = qw ( January February March April May June
301             July August September October November December );
302             my @gMoYs = map { uc (substr($_,0,3)) } @gMoY;
303             my @gDsuf = sort { my ($x,$y) = ($a,$b); $x=~s/\D+$//; $y=~s/\D+$//; $x<=>$y } grep (/^\d+\D+$/, keys %Days, "0th");
304             my @gDoW = qw( Sunday Monday Tuesday Wednesday Thursday Friday Saturday );
305             my @gDoWs = map { uc (substr($_,0,3)) } @gDoW;
306              
307              
308             # ==============================================================
309             # Not in pod on purpose. Only added to simplify test cases.
310             sub _date_language_installed
311             {
312 6     6   2703 return ( scalar (keys %date_language_installed_languages) );
313             }
314              
315             # ==============================================================
316             # Not in pod on purpose. Only added to simplify test cases.
317             sub _date_manip_installed
318             {
319 0     0   0 return ( scalar (keys %date_manip_installed_languages) );
320             }
321              
322             # ==============================================================
323              
324             =item @languages = get_languages ( );
325              
326             This module returns a sorted list of languages supported by this module
327             for the parsing of date strings.
328              
329             If neither L and/or L are installed, only
330             I is supported and you'll be unable to swap languages.
331              
332             Both modules are used since each language module supports a different
333             set of languages with a lot of overlap between them.
334              
335             Also L supports common aliases for some languages. These
336             aliases appear in lower case. When these aliases are used by
337             swap_language, it returns the real underlying language instead of
338             the alias.
339              
340             =cut
341              
342             sub get_languages
343             {
344 0     0 1 0 DBUG_ENTER_FUNC ( @_ );
345              
346 0         0 my %languages;
347              
348             # For Date::Language ... (straight forward)
349 0         0 foreach my $k1 ( keys %date_language_installed_languages ) {
350 0         0 my $lang = $date_language_installed_languages{$k1}->{Language};
351 0         0 $languages{$lang} = 1;
352             }
353              
354             # For Date::Manip ... (a bit messy)
355             # Messy since we can't verify the language without 1st loading it!
356 0         0 foreach my $k1 ( keys %date_manip_installed_languages ) {
357 0         0 my $lang = $date_manip_installed_languages{$k1}->{Language};
358 0 0       0 my $k2 = ($k1 eq lc($lang)) ? $lang : $k1;
359 0         0 $languages{$k2} = 1;
360             }
361              
362 0 0       0 if ( scalar ( keys %languages ) == 0 ) {
363 0         0 $languages{English} = 1;
364             }
365              
366 0         0 DBUG_RETURN ( sort keys %languages );
367             }
368              
369             # ==============================================================
370             # Done this way to the warning goes to fish no matter what.
371             sub _warn_msg
372             {
373 4     4   16 DBUG_ENTER_FUNC ( @_ );
374 4         2995 my $ok = shift;
375 4         12 my $msg = shift;
376 4 100       16 if ( $ok ) {
377 2         81 warn "==> ${msg}\n";
378             }
379 4         22 DBUG_VOID_RETURN ();
380             }
381              
382             # ==============================================================
383             # No POD on purpose ...
384             # Does some common logic for swap_language() & init_special_date_arrays().
385             # Requires knowledge of the internals to Date::Language::
386             # in order to work.
387             # This method should avoid referencing any global variables!
388             # Returns: undef or the references to the 5 arrays!
389              
390             sub _swap_lang_common
391             {
392 114     114   6363 DBUG_ENTER_FUNC ( @_ );
393 114         22257 my $lang_ref = shift;
394 114         305 my $warn_ok = shift;
395 114   100     576 my $allow_wide = shift || 0;
396              
397 114         379 my $base = "Date::Language";
398 114         498 my $lang = $lang_ref->{Language};
399 114         676 my $module = $lang_ref->{Module};
400              
401 114         242 my %issues;
402              
403             # Check if the requested language module exists ...
404             {
405 114         305 local $SIG{__DIE__} = "";
  114         1094  
406 114         19579 my $sts = eval "require ${module}";
407 114 50       7059 unless ( $sts ) {
408 0         0 _warn_msg ( $warn_ok, "${base} doesn't recognize '${lang}' as valid!" );
409 0         0 return DBUG_RETURN ( undef, undef, undef, undef, undef, \%issues );
410             }
411             }
412              
413             # @Dsuf isn't always available for some modules & buggy for others.
414 114         9809 my @lMoY = eval "\@${module}::MoY"; # The fully spelled out Months.
415 114         8435 my @lMoYs = eval "\@${module}::MoYs"; # The legal Abbreviations.
416 114         8137 my @lDsuf = eval "\@${module}::Dsuf"; # The suffix for the Day of Month.
417 114         6600 my @lDoW = eval "\@${module}::DoW"; # The Day of Week.
418 114         6381 my @lDoWs = eval "\@${module}::DoWs"; # The Day of Week Abbreviations.
419              
420             # Detects Windows bug caused by case insensitive OS.
421             # Where the OS says the file exists, but it doesn't match the package name.
422             # Ex: Date::Language::Greek vs Date::Language::greek
423 114 0 33     905 if ( $#lMoY == -1 && $#lMoYs == -1 && $#lDsuf == -1 && $#lDoW == -1 && $#lDoWs == -1 ) {
      33        
      0        
      0        
424 0         0 _warn_msg ( $warn_ok, "${base} doesn't recognize '${lang}' as valid due to case!" );
425 0         0 return DBUG_RETURN ( undef, undef, undef, undef, undef, \%issues );
426             }
427              
428             # Add the missing end of the month for quite a few Dsuf!
429             # Uses the suffixes from the 20's.
430 114         428 my $num = @lDsuf;
431 114 100       561 if ( $num > 29 ) {
432 87         264 my $fix = $num % 10;
433 87         373 foreach ( $num..31 ) {
434 49         667 my $idx = $_ - $num + 20 + $fix;
435 49         227 $lDsuf[$_] = $lDsuf[$idx];
436 49         274 DBUG_PRINT ("FIX", "lDsuf[%d] = lDsuf[%d] = %s (%s)",
437             $_, $idx, $lDsuf[$_], $lang);
438             }
439             }
440              
441             # --------------------------------------------------
442             # Check if Unicode/Wide Chars were used ...
443 114         4872 my $wide_flag = 0;
444 114         506 foreach ( @lMoY, @lMoYs, @lDsuf, @lDoW, @lDoWs ) {
445             # my $wide = utf8::is_utf8 ($_) || 0;
446 7137   100     19937 my $wide = ( $_ =~ m/[^\x00-\xff]/ ) || 0; # m/[^\x00-\x7f]/ doesn't completely work!
447 7137 100       11918 if ( $wide ) {
448 1031         1612 $wide_flag = 1; # Multi-byte chars detected!
449             } else {
450             # Fix so uc()/lc() work for languages like German.
451 6106         15045 utf8::encode ($_);
452 6106         12691 utf8::decode ($_); # Sets utf8 flag ...
453              
454             # Are any of these common variants wide chars?
455 6106 100 66     31279 if ( $_ =~ m/[^\x00-\xff]/ ||
      66        
456             uc ($_) =~ m/[^\x00-\xff]/ ||
457             lc ($_) =~ m/[^\x00-\xff]/ ) {
458 10         22 $wide_flag = -1;
459             }
460             }
461             }
462              
463 114         599 $lang_ref->{Wide} = $wide_flag;
464              
465 114 100 100     579 if ( $wide_flag && ! $allow_wide ) {
466 1         8 _warn_msg ( $warn_ok, "'${lang}' uses Wide Chars. It's not currently enabled!" );
467 1         215 return DBUG_RETURN ( undef, undef, undef, undef, undef, \%issues );
468             }
469              
470             # Put in the number before the suffix ... (ie: nd => 2nd, rd => 3rd)
471             # Many langages built this array incorrectly & shorted it.
472 113         468 foreach ( 0..31 ) {
473 2830 100       5348 last unless ( defined $lDsuf[$_] );
474 2804         4714 $lDsuf[$_] = $_ . $lDsuf[$_];
475 2804 100       6489 $issues{dsuf_period} = 1 if ($lDsuf[$_] =~ m/[.]/ );
476             }
477              
478             # Now check if any RegExp wild cards in the value ...
479 113         412 foreach ( @lMoY, @lMoYs ) {
480 2712 50       5941 $issues{month_period} = 1 if ( $_ =~ m/[.]/ );
481             }
482              
483 113         324 foreach ( @lDoW, @lDoWs ) {
484 1582 50       3474 $issues{dow_period} = 1 if ( $_ =~ m/[.]/ );
485             }
486              
487 113         797 DBUG_RETURN ( \@lMoY, \@lMoYs, \@lDsuf, \@lDoW, \@lDoWs, \%issues );
488             }
489              
490              
491             # ==============================================================
492             # No POD on purpose ...
493             # Does some common logic for swap_language() & init_special_date_arrays().
494             # Requires knowledge of the internals to Date::Manip::Lang::
495             # in order to work.
496             # This method should avoid referencing any global variables!
497             # Returns: undef or the references to the 5 arrays!
498             # I would have broken it up ino multiple functions if not for the wide test!
499              
500             sub _swap_manip_language_common
501             {
502 0     0   0 DBUG_ENTER_FUNC ( @_ );
503 0         0 my $lang_ref = shift;
504 0         0 my $warn_ok = shift;
505 0   0     0 my $allow_wide = shift || 0;
506              
507 0         0 my $base = "Date::Manip";
508 0         0 my $lang = $lang_ref->{Language};
509 0         0 my $module = $lang_ref->{Module};
510              
511             # Check if the requested language module exists ...
512             {
513 0         0 local $SIG{__DIE__} = "";
  0         0  
514 0         0 my $sts = eval "require ${module}";
515 0 0       0 unless ( $sts ) {
516 0         0 _warn_msg ( $warn_ok, "${base} doesn't recognize '${lang}' as valid!" );
517 0         0 return ( DBUG_RETURN ( undef, undef, undef, undef, undef, undef, undef, undef ) );
518             }
519             }
520              
521             # Get the proper name of this language fom the module.
522 0         0 $lang_ref->{Language} = $lang = eval "\$${module}::LangName";
523              
524             # Get the language data from the module.
525 0         0 my $langData = eval "\$${module}::Language"; # A hash reference with the data!
526              
527             # The 3 return values used by swap_language () ...
528 0         0 my (%months, %days, %issues);
529              
530             # The 5 return values used by init_special_date_arrays()
531 0         0 my ( @MoY, @MoYs, @Dsuf, @DoW, @DoWs);
532              
533 0         0 my $wide = 0;
534 0         0 my $has_period = 0;
535 0         0 foreach my $month_idx (1..12) {
536 0         0 foreach my $name ( @{$langData->{month_name}->[$month_idx-1]} ) {
  0         0  
537 0         0 my ($w, $k, $pi, $pe, $alt) = _fix_key ( $name );
538 0 0       0 $wide = 1 if ($w);
539 0 0 0     0 next if ( $pe && exists $months{$alt} && $months{$alt} == $month_idx );
      0        
540 0 0 0     0 $has_period = 1 if ( $pi || $pe );
541 0         0 $months{$k} = $month_idx;
542             }
543 0         0 foreach my $abb ( @{$langData->{month_abb}->[$month_idx-1]} ) {
  0         0  
544 0         0 my ($w, $k, $pi, $pe, $alt) = _fix_key ( $abb );
545 0 0       0 $wide = 1 if ($w);
546 0 0 0     0 next if ( $pe && exists $months{$alt} && $months{$alt} == $month_idx );
      0        
547 0 0 0     0 $has_period = 1 if ( $pi || $pe );
548 0         0 $months{$k} = $month_idx;
549             }
550              
551 0         0 my $first_name = $langData->{month_name}->[$month_idx-1]->[0];
552 0         0 my $first_abb = $langData->{month_abb}->[$month_idx-1]->[0];
553 0         0 push ( @MoY, (_fix_key ($first_name, 1))[1] );
554 0         0 push ( @MoYs, (_fix_key ($first_abb, 1))[1] );
555             }
556 0         0 $issues{month_period} = $has_period;
557              
558 0         0 $has_period = 0;
559 0         0 foreach my $day_idx (1..31) {
560 0         0 foreach my $day ( @{$langData->{nth}->[$day_idx-1]} ) {
  0         0  
561 0         0 my ($w, $k, $pi, $pe, $alt) = _fix_key ( $day );
562 0 0       0 $wide = 1 if ($w);
563 0 0 0     0 next if ( $pe && exists $days{$alt} && $days{$alt} == $day_idx );
      0        
564 0 0 0     0 $has_period = 1 if ( $pi || $pe );
565 0         0 $days{$k} = $day_idx;
566             }
567              
568 0         0 my $first = $langData->{nth}->[$day_idx-1]->[0];
569 0         0 push ( @Dsuf, (_fix_key ($first, 1))[1] );
570             }
571 0         0 $issues{dsuf_period} = $has_period;
572              
573             # Need Sunday to Saturday to be consistent with localime() & Date::Language.
574             # But this array is Monday to Sunday!
575             # So take advantage of -1 being last element in array to fix!
576 0         0 $has_period = 0;
577 0         0 foreach my $wd_idx (1..7) {
578 0         0 my $wd = $langData->{day_name}->[$wd_idx - 2]->[0];
579 0         0 my ($w, $k, $pi, $pe, $alt) = _fix_key ( $wd, 1 );
580 0 0       0 $wide = 1 if ($w);
581 0         0 push (@DoW, $k);
582              
583 0         0 $wd = $langData->{day_abb}->[$wd_idx - 2]->[0];
584 0         0 ($w, $k, $pi, $pe, $alt) = _fix_key ( $wd, 1 );
585 0 0       0 $wide = 1 if ($w);
586 0         0 push (@DoWs, $k);
587             }
588 0         0 $issues{dow_period} = $has_period;
589              
590 0         0 $lang_ref->{Wide} = $wide;
591              
592 0 0 0     0 if ( $wide && ! $allow_wide ) {
593 0         0 _warn_msg ( $warn_ok, "'${lang}' uses Wide Chars. It's not currently enabled!" );
594 0         0 return ( DBUG_RETURN ( undef, undef, undef, undef, undef, undef, undef, undef ) );
595             }
596              
597 0         0 DBUG_RETURN ( \%months, \%days, \%issues, \@MoY, \@MoYs, \@Dsuf, \@DoW, \@DoWs);
598             }
599              
600             # ==============================================================
601             # So uc() & lc() works against all language values ...
602             sub _fix_key
603             {
604 0     0   0 my $value = shift;
605 0   0     0 my $keep_case = shift || 0;
606              
607 0 0       0 my $wide = ( $value =~ m/[^\x00-\xff]/ ) ? 1 : 0; # Before ...
608              
609 0 0       0 unless ( $wide ) {
610 0         0 utf8::encode ($value);
611 0         0 utf8::decode ($value);
612              
613             # Now verify if any of the following makes it wide ...
614 0 0 0     0 if ( $value =~ m/[^\x00-\xff]/ ||
      0        
615             lc ($value) =~ m/[^\x00-\xff]/ ||
616             uc ($value) =~ m/[^\x00-\xff]/ ) {
617 0         0 $wide = 1;
618             }
619             }
620              
621 0 0       0 $value = lc ($value) unless ( $keep_case );
622 0         0 my $alt = $value;
623              
624 0         0 my ($has_internal_period, $has_ending_period) = (0, 0);
625 0 0       0 if ( $value =~ m/([.]?)[^.]*(.)$/ ) {
626 0 0       0 $has_internal_period = 1 if ($1 eq '.');
627 0 0       0 if ($2 eq '.') {
628 0         0 $has_ending_period = 1;
629 0         0 $alt =~ s/[.]$//;
630             }
631             }
632              
633 0         0 return ($wide, lc $value, $has_internal_period, $has_ending_period, $alt);
634             }
635              
636             # ==============================================================
637             # It's a mess since Date::Manip allows for aliases.
638              
639             sub _select_language
640             {
641 210     210   969 DBUG_ENTER_FUNC ( @_ );
642 210         97269 my $lang = shift;
643 210         550 my $warn_ok = shift;
644 210         572 my $allow_wide = shift;
645              
646 210         720 my $k = lc ($lang);
647 210         642 my $manip_ref = $date_manip_installed_languages{$k};
648 210         696 my $lang_ref = $date_language_installed_languages{$k};
649              
650 210 50 33     1048 if ( $manip_ref && ! $lang_ref ) {
651 0         0 $k = lc ($manip_ref->{Language});
652 0         0 $lang_ref = $date_language_installed_languages{$k};
653             }
654              
655 210 100 66     887 unless ( $lang_ref || $manip_ref ) {
656 2         15 _warn_msg ( $warn_ok, "Language '$lang' does not exist! So can't swap to it!" );
657 2         405 return DBUG_RETURN ( undef, undef );
658             }
659              
660 208 100       703 unless ( $allow_wide ) {
661 170 0 33     528 $manip_ref = undef if ( $manip_ref && $manip_ref->{Wide} );
662 170 100 66     1199 $lang_ref = undef if ( $lang_ref && $lang_ref->{Wide} );
663              
664 170 100 66     681 unless ( $lang_ref || $manip_ref ) {
665 1         10 _warn_msg ( $warn_ok, "Language '$lang' uses Wide Chars. It's not currently enabled!" );
666 1         206 return DBUG_RETURN ( undef, undef );
667             }
668             }
669              
670 207         814 DBUG_RETURN ( $manip_ref, $lang_ref );
671             }
672              
673             # ==============================================================
674              
675             =item $lang = swap_language ( $language[, $give_warning[, $wide]] );
676              
677             This method allows you to change the I<$language> used when this module parses
678             a date string if you have modules L and/or L
679             installed. But if neither are installed, only dates in B are
680             supported. If a language is defined in both places the results are merged.
681              
682             It always returns the active language. So if I<$language> is B or
683             invalid, it will return the current language from before the call. But if the
684             language was successfully changed, it will return the new I<$language> instead.
685              
686             Should the change fail and I<$give_warning> is set to a non-zero value, it will
687             write a warning to your screen telling you why it failed.
688              
689             So assuming one of the language modules are installed, it asks it for the list
690             of months in the requested language. And once that list is retrieved only
691             months in that language are supported when parsing a date string.
692              
693             Languages like 'Greek' that rely on I require the I<$wide> flag set
694             to true. Otherwise that language is disabled. Using the I option
695             when creating the Advanced::Config object causes the I<$wide> flag to be set to
696             B<1>.
697              
698             =cut
699              
700             # NOTE: Sets the following global variables for use by parse_date() ...
701             # %last_language_edit_flags
702             # %Months
703             # %Days
704              
705             sub swap_language
706             {
707 2648     2648 1 312673 DBUG_ENTER_FUNC ( @_ );
708 2648         533513 my $lang = shift;
709 2648         7478 my $warn_ok = shift;
710 2648   100     10510 my $allow_wide = shift || 0;
711              
712 2648 100 66     24287 if ( (! defined $lang) || lc($lang) eq lc($last_language_edit_flags{language}) ) {
713 2586         9229 return DBUG_RETURN ( $last_language_edit_flags{language} );
714             }
715              
716 62         317 my ($manip_ref, $lang_ref) = _select_language ($lang, $warn_ok, $allow_wide);
717              
718 62 100 66     9378 unless ( $lang_ref || $manip_ref ) {
719 3         17 return DBUG_RETURN ( $last_language_edit_flags{language} );
720             }
721              
722 59         175 my ($month_ref, $day_ref, $issue1_ref);
723 59 50       201 if ( $manip_ref ) {
724 0         0 my $old = $manip_ref->{Language};
725 0         0 ($month_ref, $day_ref, $issue1_ref) =
726             _swap_manip_language_common ($manip_ref, $warn_ok, $allow_wide );
727 0         0 $lang = $manip_ref->{Language};
728              
729 0 0 0     0 if ( $old ne $lang && ! $lang_ref ) {
730 0         0 $lang_ref = $date_language_installed_languages{lc($lang)};
731 0 0 0     0 $lang_ref = undef if ($lang_ref && $lang_ref->{Wide} && ! $allow_wide);
      0        
732             }
733             }
734              
735 59         133 my ($MoY_ref, $MoYs_ref, $Dsuf_ref, $issue2_ref);
736 59 50       172 if ( $lang_ref ) {
737 59         147 my ($unused_DoW_ref, $unused_DoWs_ref);
738 59         353 ($MoY_ref, $MoYs_ref, $Dsuf_ref, $unused_DoW_ref, $unused_DoWs_ref, $issue2_ref) =
739             _swap_lang_common ( $lang_ref, $warn_ok, $allow_wide );
740 59         9496 $lang = $lang_ref->{Language};
741             }
742              
743 59 100 66     333 unless ( $MoY_ref || $month_ref ) {
744 1         6 return DBUG_RETURN ( $last_language_edit_flags{language} );
745             }
746              
747             DBUG_PRINT ("SWAP", "Swapping from '%s' to '%s'.",
748 58         307 $last_language_edit_flags{language}, $lang);
749              
750             # ---------------------------------------------------------
751 58         6058 foreach my $k ( keys %last_language_edit_flags ) {
752 232   100     1370 $last_language_edit_flags{$k} = $issue1_ref->{$k} || $issue2_ref->{$k} || 0;
753             }
754 58         197 $last_language_edit_flags{language} = $lang;
755              
756             # ---------------------------------------------------------
757             # Bug Alert: For some languges the following isn't true!
758             # lc(MoY) != lc(uc(lc(MoY)))
759             # So we have multiple lower case letters mapping to the
760             # same upper case letters#.
761             # ---------------------------------------------------------
762             # This happens for 3 languages for Date::Language.
763             # Chinese_GB, Greek & Russian_cp1251
764             # And one language for Date::Manip
765             # Turkish
766             # ---------------------------------------------------------
767              
768 58         113 my %empty;
769 58         2994 %Months = %Days = %empty;
770              
771             # ---------------------------------------------------------
772             # Put in the common numeric values into the hashes ...
773 58         130 my $cnt;
774 58         204 foreach $cnt ( 1..12 ) {
775 696         2401 $Months{$cnt} = $cnt;
776             }
777              
778 58         157 foreach my $day ( 1..31 ) {
779 1798         3721 $Days{$day} = $day;
780             }
781              
782             # ---------------------------------------------------------
783             # Merge in the Date::Manip::Lang:: values ...
784              
785 58         142 foreach my $mon ( keys %{$month_ref} ) {
  58         331  
786 0         0 $Months{$mon} = $month_ref->{$mon};
787 0         0 $Months{lc (uc (lc ($mon)))} = $Months{$mon}; # Bug fix, but usually same.
788             }
789              
790 58         129 foreach my $day ( keys %{$day_ref} ) {
  58         256  
791 0         0 $Days{$day} = $day_ref->{$day};
792 0         0 $Days{lc (uc (lc ($day)))} = $Days{$day}; # Bug fix, but usually same.
793             }
794              
795             # ---------------------------------------------------------
796             # Merge in the Date::Language:: values ...
797              
798 58         171 $cnt = 1;
799 58         113 foreach my $mon ( @{$MoY_ref} ) {
  58         164  
800 696         3982 $Months{lc ($mon)} = $cnt;
801 696         1754 $Months{lc (uc (lc ($mon)))} = $cnt; # Bug fix, but usually same.
802 696         1053 ++$cnt;
803             }
804              
805 58         140 $cnt = 1;
806 58         128 foreach my $mon ( @{$MoYs_ref} ) {
  58         157  
807 696         1597 $Months{lc ($mon)} = $cnt;
808 696         1397 $Months{lc (uc (lc ($mon)))} = $cnt; # Bug fix, but usually same.
809 696         1035 ++$cnt;
810             }
811              
812 58         164 foreach my $day ( 1..31 ) {
813 1798 100 66     5335 if ( $Dsuf_ref && defined $Dsuf_ref->[$day] ) {
814 1397         2276 my $key = $Dsuf_ref->[$day];
815 1397         3739 $Days{lc ($key)} = $day;
816 1397         2631 $Days{lc (uc (lc ($key)))} = $day; # Bug fix, but usually same.
817             }
818             }
819              
820             # ---------------------------------------------------------
821             # Report the results ...
822              
823             DBUG_PRINT ( "LANGUAGE", "%s\n%s\n%s",
824 7818 50       17863 join (", ", sort { $Months{$a} <=> $Months{$b} || $a cmp $b } keys %Months),
825 58 50       1177 join (", ", sort { my ($x,$y) = ($a,$b); $x=~s/\D+//g; $y=~s/\D+//g; $x=0 if ($x eq ""); $y=0 if ($y eq ""); ($x<=>$y || $a cmp $b) } keys %Days),
  14550 50       24317  
  14550 50       27001  
  14550         26745  
  14550         26707  
  14550         25565  
  14550         30674  
826             join (", ", %last_language_edit_flags) );
827              
828 58         7979 DBUG_RETURN ( $lang );
829             }
830              
831              
832             # ==============================================================
833              
834             =item $date = parse_date ( $date_str, $order[, $allow_dl[, $enable_2_digit_years]] );
835              
836             Passed a date in some unknown format, it does it's best to parse it and return
837             the date in S format if it's a valid date. It returns B if
838             it can't find a valid date within I<$date_str>.
839              
840             The date can be surrounded by other information in the string that will be
841             ignored. So it will strip out just the date info in something like:
842              
843             =over 4
844              
845             Tues B at 6:00 PM.
846              
847             =back
848              
849             There are too many valid date formats to list them all, especially when other
850             languages are added to the mix. But if you have one it doesn't support, open
851             a CPAN ticket and I'll see if I can quickly add it.
852              
853             I<$order> tells the order to use for interpreting dates that are all digits.
854             It's forwarded to all internal calls to L and
855             L. So see those methods POD for more info on its meaning.
856              
857             I<$allow_dl> is non-zero and L is installed use it's method
858             B to attempt the conversion only if nothing else worked.
859              
860             If I<$enable_2_digit_years> is set to zero, it will not recognize any 2-digit
861             year date formats as valid. Set to a non-zero value to enable them.
862              
863             =cut
864              
865             # Check out Date::Parse for date examples to use to test this function out.
866              
867             sub lcx
868             {
869 8476     8476 0 14719 my $str = shift;
870              
871 8476 100       29908 unless ( utf8::is_utf8 ($str) ) {
872 2588         7090 utf8::encode ($str);
873 2588         6083 utf8::decode ($str);
874             }
875              
876 8476         33527 return (lc ($str));
877             }
878              
879             sub _tst
880             {
881 0     0   0 my $s = shift;
882 0         0 my $nm = shift;
883 0         0 my $dm = shift;
884 0         0 DBUG_PRINT ("TST", "Matched Pattern (%s) Sep: %s Name: %s Dom: %s", join (",",@_), $s, $nm, $dm);
885 0         0 return (1);
886             }
887              
888             # DEPRECIATED VERSION ...
889             sub parse_date_old
890             {
891 0     0 0 0 DBUG_ENTER_FUNC ( @_ );
892 0         0 my $in_date = shift; # A potential date in an unknown format ...
893 0         0 my $date_format_options = shift; # A comma separated list of ids ...
894 0   0     0 my $use_date_language_module = shift || 0;
895 0   0     0 my $allow_2_digit_years = shift || 0;
896              
897             # The Month name pattern, ... [a-zA-Z] doesn't work for other languages.
898 0         0 my $name = "[^-\$\\s\\d.,|\\[\\]\\\\/{}()]";
899              
900             # The Day of Month pattern ... (when not all digits are expected)
901 0         0 my $dom = "\\d{0,2}${name}*";
902              
903             # Remove the requesed character from the month pattern ...
904 0 0       0 $name =~ s/\\s//g if ( $last_language_edit_flags{month_spaces} );
905 0 0       0 $name =~ s/[.]//g if ( $last_language_edit_flags{month_period} );
906 0 0       0 $name =~ s/-//g if ( $last_language_edit_flags{month_hyphin} );
907              
908 0         0 $name .= '+'; # Terminate the name pattern.
909              
910             # Remove the requesed character from the day of month pattern ...
911 0 0       0 $dom =~ s/\\s//g if ( $last_language_edit_flags{dsuf_spaces} );
912 0 0       0 $dom =~ s/[.]//g if ( $last_language_edit_flags{dsuf_period} );
913 0 0       0 $dom =~ s/-//g if ( $last_language_edit_flags{dsuf_hyphin} );
914              
915 0         0 my ( $year, $month, $day );
916 0         0 my ( $s1, $s2 ) = ( "", "" );
917 0         0 my $fmt = "n/a";
918              
919             # The 7 separators to cycle through to parse things correctly ...
920 0         0 my @seps = ( "-", "/", "[.]", ",", "\\s+", '\\\\', ":" );
921              
922             # -------------------------------------------------------
923             # Let's start with the 4-digit year formats ...
924             # -------------------------------------------------------
925 0         0 foreach my $sep ( @seps ) {
926 0 0 0     0 if ( $in_date =~ m/(^|\D)(\d{4})(${sep})(\d{1,2})(${sep})(\d{1,2})(\D|$)/ ) {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
      0        
927 0         0 ( $year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
928 0         0 $fmt = "YYYY${s1}MM${s2}DD"; # ISO format
929              
930             } elsif ( $in_date =~ m/(^|\D)(\d{1,2})(${sep})(\d{1,2})(${sep})(\d{4})(\D|$)/ ) {
931 0         0 ( $month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
932 0         0 ( $year, $month, $day ) = parse_8_digit_date ( sprintf ("%02d%02d%04d", $month, $day, $year),
933             $date_format_options, 1 );
934 0         0 $fmt = "MM${s1}DD${s2}YYYY"; # European or American format (ambiguous?)
935              
936             # ------------------------------------------------------------------------------------------
937             } elsif ( $in_date =~ m/(^|\D)(\d{1,2})(${sep})(${name})[.]?(${sep})(\d{4})(\D|$)/ &&
938             exists $Months{lcx($4)} ) {
939 0         0 ( $day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 );
940 0         0 $fmt = "DD${s1}Month${s2}YYYY";
941              
942             } elsif ( $in_date =~ m/(^|\D)(\d{4})(${sep})(${name})[.]?(${sep})(\d{1,2})(\D|$)/ &&
943             exists $Months{lcx($4)} ) {
944 0         0 ( $year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
945 0         0 $fmt = "YYYY${s1}Month${s2}DD";
946              
947             } elsif ( $in_date =~ m/(^|\s)(${name})(${sep})(\d{1,2})(${sep})(\d{4})(\D|$)/ &&
948             exists $Months{lcx($2)} ) {
949 0         0 ( $month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
950 0         0 $fmt = "Month${s1}DD${s2}YYYY";
951              
952             # ------------------------------------------------------------------------------------------
953             } elsif ( $in_date =~ m/(^|\s)(${dom})(${sep})(${name})[.]?(${sep})(\d{4})(\D|$)/ &&
954             exists $Months{lcx($4)} &&
955             exists $Days{lcx($2)} ) {
956 0         0 ( $day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 );
957 0         0 $fmt = "Day${s1}Month${s2}YYYY"; # European format
958              
959             } elsif ( $in_date =~ m/(^|\D)(\d{4})(${sep})(${name})[.]?(${sep})(${dom})(\s|$)/ &&
960             exists $Months{lcx($4)} &&
961             exists $Days{lcx($6)} ) {
962 0         0 ( $year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
963 0         0 $fmt = "YYYY${s1}Month${s2}Day"; # ISO format
964              
965             } elsif ( $in_date =~ m/(^|\s)(${name})(${sep})(${dom})(${sep})(\d{4})(\D|$)/ &&
966             exists $Months{lcx($2)} &&
967             exists $Days{lcx($4)} ) {
968 0         0 ( $month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
969 0         0 $fmt = "Month${s1}Day${s2}YYYY"; # American format
970             }
971              
972 0 0       0 last if ( defined $year );
973             }
974              
975 0 0 0     0 if ( defined $year ) {
    0 0        
    0          
    0          
    0          
976             ; # No more formatting tests needed ...
977              
978             # "Month Day, YYYY" or "Month Day YYYY"
979             } elsif ( $in_date =~ m/(${name})[.\s]\s*(${dom})[,\s]\s*(\d{4})(\D|$)/ &&
980             exists $Months{lcx($1)} ) {
981 0         0 ( $month, $day, $year ) = ( $1, $2, $3 );
982 0         0 $fmt = "Month Day, YYYY";
983              
984             # "Month Day, HH:MM:SS YYYY" or "Month Day HH:MM:SS YYYY"
985             # Added because: "$dt = localtime(time())" generates this format.
986             } elsif ( $in_date =~ m/(${name})[.]?\s+(${dom})[,\s]\s*(\d{1,2}:\d{1,2}(:\d{1,2})?)\s+(\d{4})(\D|$)/ &&
987             exists $Months{lcx($1)} ) {
988 0         0 my $time;
989 0         0 ( $month, $day, $time, $year ) = ( $1, $2, $3, $5 );
990 0         0 $fmt = "Month Day HH:MM[:SS] YYYY";
991              
992             # As a string of 8 digits.
993             } elsif ( $in_date =~ m/(^|\D)(\d{8})(\D|$)/ ) {
994 0         0 ($year, $month, $day) = parse_8_digit_date ( $2, $date_format_options, 0 );
995 0         0 $fmt = "YYYYMMDD";
996              
997             # -------------------------------------------------------
998             # Finally, assume it's using a 2-digit year format ...
999             # Only if they are allowed ...
1000             # -------------------------------------------------------
1001             } elsif ( $allow_2_digit_years ) {
1002 0         0 foreach my $sep ( @seps ) {
1003 0 0       0 next if ( $sep eq ":" ); # Skip, if used it looks like a time of day ...
1004              
1005 0 0 0     0 if ( $in_date =~ m/(^|[^:\d])(\d{1,2})(${sep})(\d{1,2})(${sep})(\d{1,2})([^:\d]|$)/ ) {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
      0        
      0        
      0        
      0        
      0        
1006 0         0 ($s1, $s2) = ($3, $5);
1007 0         0 my $yymmdd = sprintf ("%02d%02d%02d", $2, $4, $6);
1008 0         0 ($year, $month, $day) = parse_6_digit_date ( $yymmdd, $date_format_options );
1009 0         0 $fmt = "YY${s1}MM${s2}DD ???";
1010              
1011             # ------------------------------------------------------------------------------------------
1012             } elsif ( $in_date =~ m/(^|\D)(\d{1,2})(${sep})(${name})[.]?(${sep})(\d{1,2})([^:\d]|$)/ &&
1013             exists $Months{lcx($4)} ) {
1014 0         0 ( $year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
1015 0         0 my $yymmdd = sprintf ("%02d%02d%02d", $year, $Months{lcx($month)}, $day);
1016 0         0 my @order;
1017 0         0 foreach ( split (/\s*,\s*/, $date_format_options) ) {
1018 0 0       0 push (@order, $_) if ( $_ != 2 ); # If not American format ...
1019             }
1020 0         0 ($year, $month, $day) = parse_6_digit_date ( $yymmdd, join(",", @order) );
1021 0         0 $fmt = "DD${s1}Month${s2}YY or YY${s1}Month${s2}DD";
1022              
1023             } elsif ( $in_date =~ m/(^|\s)(${name})(${sep})(\d{1,2})(${sep})(\d{1,2})([^:\d]|$)/ &&
1024             exists $Months{lcx($2)} ) {
1025 0         0 ( $month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1026 0         0 $year = make_it_a_4_digit_year ( $year );
1027 0         0 $fmt = "Month${s1}DD${s2}YY";
1028              
1029             # ------------------------------------------------------------------------------------------
1030             } elsif ( $in_date =~ m/(^|\s)(${name})[.]?(${sep})(${dom})(${sep})(\d{1,2})([^:\d]|$)/ &&
1031             _tst( $sep, $name, $dom, $2, $4, $6 ) &&
1032             exists $Months{lcx($2)} &&
1033             exists $Days{lcx($4)} ) {
1034 0         0 ( $month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1035 0         0 $year = make_it_a_4_digit_year ( $year );
1036 0         0 $fmt = "Month${s1}Day${s2}YY"; # American format
1037              
1038             } elsif ( $in_date =~ m/(^|\s)(${dom})(${sep})(${name})[.]?(${sep})(\d{1,2})([^:\d]|$)/ &&
1039             _tst( $sep, $name, $dom, $2, $4, $6 ) &&
1040             exists $Months{lcx($4)} &&
1041             exists $Days{lcx($2)} ) {
1042 0         0 ( $day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1043 0         0 $year = make_it_a_4_digit_year ( $year );
1044 0         0 $fmt = "Day${s1}Month${s2}YY"; # European format
1045              
1046             } elsif ( $in_date =~ m/(^|\D)(\d{1,2})(${sep})(${name})[.]?(${sep})(${dom})(\s|$)/ &&
1047             _tst( $sep, $name, $dom, $2, $4, $6 ) &&
1048             exists $Months{lcx($4)} &&
1049             exists $Days{lcx($6)} ) {
1050 0         0 ( $year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
1051 0         0 $year = make_it_a_4_digit_year ( $year );
1052 0         0 $fmt = "YY${s1}Month${s2}Day"; # ISO format
1053             }
1054              
1055 0 0       0 last if ( defined $year );
1056             }
1057              
1058 0 0 0     0 if ( defined $year ) {
    0 0        
    0          
1059             ; # No more formatting tests needed ...
1060              
1061             # "Month Day, YY" or "Month Day YY"
1062             } elsif ( $in_date =~ m/(${name})[.\s]\s*(${dom})[,\s]\s*(\d{2})(\D|$)/ &&
1063             _tst( "\\s", $name, $dom, $1, $2, $3 ) &&
1064             exists $Months{lcx($1)} ) {
1065 0         0 ( $month, $day ) = ( $1, $2 );
1066 0         0 $year = make_it_a_4_digit_year ( $3 );
1067 0         0 $fmt = "Month Day, YY";
1068              
1069             # As a string of 6 digits.
1070             } elsif ( $in_date =~ m/(^|\D)(\d{6})(\D|$)/ ) {
1071 0         0 ($year, $month, $day) = parse_6_digit_date ( $2, $date_format_options );
1072 0         0 $fmt = "YYMMDD";
1073             }
1074             } # End if its a 2-digit year ...
1075              
1076              
1077             # --------------------------------------------------------------------
1078             # If my parsing didn't work try using Date::Language if it's installed.
1079             # Keep after my checks so that things are consistent when this module
1080             # isn't installed. (No way to disable 2-digit year format here.)
1081             # --------------------------------------------------------------------
1082              
1083 0 0 0     0 if ( $use_date_language_module && ! defined $year ) {
1084 0 0       0 unless ( _date_language_installed () ) {
1085 0         0 DBUG_PRINT ("INFO", "Using Date::Language::str2time was requested, but it's not installed!");
1086             } else {
1087 0         0 DBUG_PRINT ("INFO", "Using Date::Language::str2time to attempt the parse!");
1088 0         0 eval {
1089 0         0 my $dl = Date::Language->new ( $last_language_edit_flags{language} );
1090 0         0 my $t = $dl->str2time ( $in_date );
1091 0 0       0 if ( defined $t ) {
1092 0         0 ($year, $month, $day) = (localtime ($t))[5,4,3];
1093 0         0 $year += 1900;
1094 0         0 $month += 1;
1095             }
1096             };
1097             }
1098             }
1099              
1100             # --------------------------------------------------------------------
1101             # We're done with parsing things. Now let's validate the results!
1102             # --------------------------------------------------------------------
1103              
1104 0 0       0 if ( ! defined $year ) {
1105 0         0 DBUG_PRINT ("ERROR", "No such date format is supported: %s", $in_date);
1106              
1107             # Else we're using a known date format ...
1108             } else {
1109 0         0 DBUG_PRINT ("FORMAT", "%s ==> %s ==> (Y:%s, M:%s, D:%s, Sep:%s)",
1110             $fmt, $in_date, $year, $month, $day, $s1);
1111              
1112             # It's not a valid date if the separaters are different ...
1113             # Shouldn't be possible any more unless it's spaces.
1114             # (Hence we die if it happens)
1115 0 0       0 if ( $s1 ne $s2 ) {
1116 0 0 0     0 unless ( $s1 =~ m/^\s*$/ && $s2 =~ m/^\s*$/ ) {
1117 0         0 die ("BUG: Separators are different ($s1 vs $s2)\n");
1118             }
1119             }
1120              
1121             # Now let's validate the results ...
1122             # Trim leading/trailing spaces ...
1123 0 0       0 $day = $1 if ( $day =~ m/^\s*(.*)\s*$/ );
1124              
1125 0         0 return DBUG_RETURN ( _check_if_good_date ($in_date, $year, $month, $day) );
1126             }
1127              
1128 0         0 DBUG_RETURN ( undef ); # Invalid date ...
1129             }
1130              
1131              
1132             sub parse_date
1133             {
1134 2833     2833 1 336705 DBUG_ENTER_FUNC ( @_ );
1135 2833         613043 my $in_date = shift; # A potential date in an unknown format ...
1136 2833         7819 my $date_format_options = shift; # A comma separated list of fmt ids ...
1137 2833   100     15197 my $use_date_language_module = shift || 0;
1138 2833   100     8668 my $allow_2_digit_years = shift || 0;
1139              
1140 2833         9686 $in_date = lcx ($in_date); # Make sure always in lower case ...
1141              
1142 2833         10406 my ($month, $month_digits) = _find_month_in_string ( $in_date );
1143 2833 100       424867 my ($dom, $dom_digits) = _find_day_of_month_in_string ( $in_date, $month_digits,
1144             $month_digits ? undef : $month );
1145              
1146 2833         433923 my $out_str;
1147              
1148 2833 100 66     18564 if ( $month_digits && $dom_digits ) {
    50          
    100          
1149 106         453 $out_str = _month_num_day_num ( $in_date, $month, $dom, $allow_2_digit_years, $date_format_options );
1150             } elsif ( $month_digits ) {
1151 0         0 $out_str = _month_num_day_str ( $in_date, $month, $dom, $allow_2_digit_years );
1152             } elsif ( $dom_digits ) {
1153 1786         6567 $out_str = _month_str_day_num ( $in_date, $month, $dom, $allow_2_digit_years, $date_format_options );
1154             } else {
1155 941         4014 $out_str = _month_str_day_str ( $in_date, $month, $dom, $allow_2_digit_years );
1156             }
1157              
1158             # --------------------------------------------------------------------
1159             # If my parsing didn't work try using Date::Language if it's installed.
1160             # Keep after my checks so that things are consistent when this module
1161             # isn't installed. (No way to disable 2-digit year format here.)
1162             # --------------------------------------------------------------------
1163 2833 50 66     739992 if ( $use_date_language_module && (! $out_str) &&
      66        
1164             _date_language_installed () ) {
1165 2         7 DBUG_PRINT ("INFO", "Using Date::Language::str2time to attempt parsing!");
1166 2         467 eval {
1167 2         18 my $dl = Date::Language->new ( $last_language_edit_flags{language} );
1168 2         260 my $t = $dl->str2time ( $in_date );
1169 2 50       9671 if ( defined $t ) {
1170 2         22 my ($year, $month, $day) = (localtime ($t))[5,4,3];
1171 2         6 $year += 1900;
1172 2         4 $month += 1;
1173              
1174 2         9 $out_str = _check_if_good_date ($in_date, $year, $month, $day);
1175             }
1176             };
1177             }
1178              
1179 2833         8851 DBUG_RETURN ($out_str); # undef or the date in YYYY-MM-DD format.
1180             }
1181              
1182             # --------------------------------------------------------------
1183             # No ambiguity here ... we have multiple text anchors ...
1184              
1185             sub _month_str_day_str
1186             {
1187 941     941   3720 DBUG_ENTER_FUNC ( @_ );
1188 941         196558 my $in_date = shift;
1189 941         2569 my $month_str = shift;
1190 941         1970 my $dom_str = shift;
1191 941         1799 my $allow_2_digit_years = shift;
1192              
1193 941         1954 my ($year, $s1, $month, $s2, $day );
1194              
1195 941 100       128905 if ( $in_date =~ m/(^|\D)(${month_str})[.]?(.*?\D)(${dom_str})(.*?\D)(\d{4})($|\D)/ ) {
    50          
    50          
1196 477         5060 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # American format ...
1197              
1198             } elsif ($in_date =~ m/(^|\D)(${dom_str})(.+?)(${month_str})[.]?(.*?\D)(\d{4})($|\D)/ ) {
1199 0         0 ($day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # European format ...
1200              
1201             } elsif ( $in_date =~ m/(^|\D)(\d{4})(\D.*?)(${month_str})[.]?(.*?\D)(${dom_str})($|\D)/ ) {
1202 0         0 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 ); # ISO format ...
1203             }
1204              
1205 941 100 66     9114 if ( $allow_2_digit_years && ! defined $year ) {
1206 464 50       45594 if ( $in_date =~ m/(^|\D)(${month_str})[.]?(.*?\D)(${dom_str})(.*?[^:\d])(\d{2})($|[^:\d])/ ) {
    0          
    0          
1207 464         5094 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # American format ...
1208              
1209             } elsif ($in_date =~ m/(^|\D)(${dom_str})(.+?)(${month_str})[.]?(.*?[^:\d])(\d{2})($|[^:\d])/ ) {
1210 0         0 ($day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # European format ...
1211              
1212             } elsif ( $in_date =~ m/(^|[^:\d])(\d{2})([^:\d].*?)(${month_str})[.]?(.*?\D)(${dom_str})($|\D)/ ) {
1213 0         0 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 ); # ISO format ...
1214             }
1215              
1216 464 50       2975 $year = make_it_a_4_digit_year ( $year ) if (defined $year);
1217             } # End if allowing 2-digit years ...
1218              
1219 941 50       61289 if ( defined $year ) {
1220 941         3506 return DBUG_RETURN ( _check_if_good_date ($in_date, $year, $month, $day) );
1221             }
1222              
1223 0         0 DBUG_RETURN ( undef );
1224             }
1225              
1226             # --------------------------------------------------------------
1227             # With a month anchor still not too ambiguous.
1228              
1229             sub _tst_4_YY
1230             {
1231 235     235   1224 my $sep = shift;
1232 235 100       812 my $res = ( $sep =~ m/\s\d{1,2}\s/ ) ? 0 : 1;
1233 235         1879 return ($res);
1234             }
1235              
1236             sub _month_str_day_num
1237             {
1238 1786     1786   7502 DBUG_ENTER_FUNC ( @_ );
1239 1786         401581 my $in_date = shift;
1240 1786         4532 my $month_str = shift;
1241 1786         4068 my $dom_num = shift;
1242 1786         3847 my $allow_2_digit_years = shift;
1243 1786         3695 my $date_format_options = shift;
1244              
1245 1786         3776 my ($year, $s1, $month, $s2, $day );
1246              
1247             # American format ...
1248 1786 100 66     608197 if ( $in_date =~ m/(^|\D)(${month_str})[.]?([^\d]*?\D)(${dom_num})(\D)(\d{4})($|\D)/ ) {
    100 100        
    100          
    100          
1249 76         609 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1250 76         408 DBUG_PRINT ("AMERICAN-1", "${month}/${day}/${year} -- ($s1) ($s2)");
1251              
1252             # American format ...
1253             } elsif ( $in_date =~ m/(^|\D)(${month_str})[.]?([^\d]*?\D)(${dom_num})(\D.*?\D)(\d{4})($|\D)/ &&
1254             _tst_4_YY ( $5 ) ) {
1255 206         1393 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1256 206         1227 DBUG_PRINT ("AMERICAN-2", "${month}/${day}/${year} -- ($s1) ($s2)");
1257              
1258             # European format ...
1259             } elsif ($in_date =~ m/(^|\D)(${dom_num})(\D*?)(${month_str})[.]?(.*?\D)(\d{4})($|\D)/ &&
1260             _tst_4_YY ( $5 ) ) {
1261 21         139 ($day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1262 21         159 DBUG_PRINT ("EUROPEAN", "${day}/${month}/${year} -- ($s1) ($s2)");
1263              
1264             # ISO format ...
1265             } elsif ( $in_date =~ m/(^|\D)(\d{4})(\D*?)(${month_str})[.]?(.*?\D)(${dom_num})($|\D)/ ) {
1266 636         7531 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
1267 636         4440 DBUG_PRINT ("ISO", "${year}/${month}/${day} -- ($s1) ($s2)");
1268             }
1269              
1270 1786 100 100     138766 if ( $allow_2_digit_years && ! defined $year ) {
1271             # American format ...
1272 847 100 100     421704 if ( $in_date =~ m/(^|\D)(${month_str})[.]?(.*?[^:\d])(${dom_num})([^:\d])(\d{2})($|[^:\d])/ ||
    100          
    100          
    50          
1273             $in_date =~ m/(^|\D)(${month_str})[.]?(.*?[^:\d])(${dom_num})([^:\d].*?[^:\d])(\d{2})($|[^:\d])/ ) {
1274 182         1888 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1275 182         840 $year = make_it_a_4_digit_year ( $year );
1276              
1277             # Ambiguous ... Either ISO or European, so must use hint ...
1278             } elsif ($in_date =~ m/(^|\D)(${dom_num})([^:\d].*?)(${month_str})[.]?(.*?[^:\d])(${dom_num})($|[^:\d])/ ) {
1279 9         86 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
1280 9         34 my $yymmdd = sprintf ("%02d%02d%02d", $year, $Months{lcx($month)}, $day);
1281 9         19 my @order;
1282 9         76 foreach ( split (/\s*,\s*/, $date_format_options) ) {
1283 27 100       87 push (@order, $_) if ( $_ != 2 ); # Drop American format ...
1284             }
1285 9         68 ($year, $month, $day) = parse_6_digit_date ( $yymmdd, join(",", @order) );
1286              
1287             # European format ...
1288             } elsif ($in_date =~ m/(^|\D)(${dom_num})([^:\d].*?)(${month_str})[.]?(.*?[^:\d])(\d{2})($|[^:\d])/ ) {
1289 652         6749 ($day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 );
1290 652         2382 $year = make_it_a_4_digit_year ( $year );
1291              
1292             # ISO format ...
1293             } elsif ( $in_date =~ m/(^|[^:\d])(\d{2})([^:\d].*?)(${month_str})[.]?(.*?[^:\d])(${dom_num})($|[^:\d])/ ) {
1294 0         0 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
1295 0         0 $year = make_it_a_4_digit_year ( $year );
1296             }
1297             } # End if allowing 2-digit years ...
1298              
1299 1786 100       119417 if ( defined $year ) {
1300 1782         6181 return DBUG_RETURN ( _check_if_good_date ($in_date, $year, $month, $day) );
1301             }
1302              
1303 4         15 DBUG_RETURN ( undef );
1304             }
1305              
1306             # --------------------------------------------------------------
1307             # Getting a bit more problematic ...
1308              
1309             sub _month_num_day_str
1310             {
1311 0     0   0 DBUG_ENTER_FUNC ( @_ );
1312 0         0 my $in_date = shift;
1313 0         0 my $month_num = shift;
1314 0         0 my $dom_str = shift;
1315 0         0 my $allow_2_digit_years = shift;
1316              
1317 0         0 my ($year, $s1, $month, $s2, $day );
1318              
1319 0 0 0     0 if ( $in_date =~ m/(^|[^:\d])(${month_num})(\D)(${dom_str})(.*?\D)(\d{4})($|\D)/ ||
    0 0        
    0 0        
      0        
      0        
1320             $in_date =~ m/(^|[^:\d])(${month_num})(\D.*?\D)(${dom_str})(.*?\D)(\d{4})($|\D)/ ) {
1321 0         0 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # American format ...
1322              
1323             } elsif ($in_date =~ m/(^|\D)(${dom_str})(.*?\D)(${month_num})(\D)(\d{4})($|\D)/ ||
1324             $in_date =~ m/(^|\D)(${dom_str})(.*?\D)(${month_num})(\D.*?\D)(\d{4})($|\D)/ ) {
1325 0         0 ($day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # European format ...
1326              
1327             } elsif ( $in_date =~ m/(^|\D)(\d{4})(\D)(${month_num})(\D)(${dom_str})($|\D)/ ||
1328             $in_date =~ m/(^|\D)(\d{4})(\D)(${month_num})(\D.*?\D)(${dom_str})($|\D)/ ||
1329             $in_date =~ m/(^|\D)(\d{4})(\D.*?\D)(${month_num})(\D)(${dom_str})($|\D)/ ||
1330             $in_date =~ m/(^|\D)(\d{4})(\D.*?\D)(${month_num})(\D.*?\D)(${dom_str})($|\D)/ ) {
1331 0         0 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 ); # ISO format ...
1332             }
1333              
1334 0 0 0     0 if ( $allow_2_digit_years && ! defined $year ) {
1335 0 0 0     0 if ( $in_date =~ m/(^|\D)(${month_num})([^:\d])(${dom_str})(.*?[^:\d])(\d{2})($|[^:\d])/ ||
    0 0        
    0 0        
      0        
      0        
1336             $in_date =~ m/(^|\D)(${month_num})([^:\d].*?[^:\d])(${dom_str})(.*?[^:\d])(\d{2})($|[^:\d])/ ) {
1337 0         0 ($month, $s1, $day, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # American format ...
1338              
1339             } elsif ($in_date =~ m/(^|\D)(${dom_str})(.*?[^:\d])(${month_num})([^:\d])(\d{2})($|[^:\d])/ ||
1340             $in_date =~ m/(^|\D)(${dom_str})(.*?[^:\d])(${month_num})([^:\d].*?[^:\d])(\d{2})($|[^:\d])/ ) {
1341 0         0 ($day, $s1, $month, $s2, $year ) = ( $2, $3, $4, $5, $6 ); # European format ...
1342              
1343             } elsif ( $in_date =~ m/(^|[^:\d])(\d{2})([^:\d])(${month_num})([^:\d])(${dom_str})($|\D)/ ||
1344             $in_date =~ m/(^|[^:\d])(\d{2})([^:\d])(${month_num})([^:\d].*?[^:\d])(${dom_str})($|\D)/ ||
1345             $in_date =~ m/(^|[^:\d])(\d{2})([^:\d].*?[^:\d])(${month_num})([^:\d])(${dom_str})($|\D)/ ||
1346             $in_date =~ m/(^|[^:\d])(\d{2})([^:\d].*?[^:\d])(${month_num})([^:\d].*?[^:\d])(${dom_str})($|\D)/ ) {
1347 0         0 ($year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 ); # ISO format ...
1348             }
1349              
1350 0 0       0 $year = make_it_a_4_digit_year ( $year ) if (defined $year);
1351             } # End if allowing 2-digit years ...
1352              
1353 0 0       0 if ( defined $year ) {
1354 0         0 return DBUG_RETURN ( _check_if_good_date ($in_date, $year, $month, $day) );
1355             }
1356              
1357 0         0 DBUG_RETURN ( undef );
1358             }
1359              
1360             # --------------------------------------------------------------
1361             # A very ambiguous format ... and much, much messier!
1362              
1363             sub _month_num_day_num
1364             {
1365 106     106   462 DBUG_ENTER_FUNC ( @_ );
1366 106         65828 my $in_date = shift;
1367 106         252 my $month_num = shift;
1368 106         240 my $dom_num = shift;
1369 106         189 my $allow_2_digit_years = shift;
1370 106         233 my $date_format_options = shift;
1371              
1372 106         259 my ($year, $s1, $month, $s2, $day );
1373              
1374             # Unknown format, use hint to decide ...
1375 106 100       2020 if ( $in_date =~ m/(^|\D)(\d{8})($|\D)/ ) {
    100          
    100          
1376 12         53 ( $year, $month, $day ) = parse_8_digit_date ( $2, $date_format_options, 0 );
1377 12         3266 $s1 = $s2 = "";
1378              
1379             # American or European Format, use hint to decide ...
1380             } elsif ( $in_date =~ m/(^|\D)(\d{1,2})(\D+)(\d{1,2})(\D+)(\d{4})(\D|$)/ ) {
1381 42         242 ( $s1, $s2 ) = ( $3, $5 );
1382 42         473 my $date = sprintf ("%02d%02d%04d", $2, $4, $6);
1383 42         197 ( $year, $month, $day ) = parse_8_digit_date ( $date, $date_format_options, 1 );
1384              
1385             # ISO Format ...
1386             } elsif ( $in_date =~ m/(^|\D)(\d{4})(\D+)(${month_num})(\D+)(${dom_num})(\D|$)/ ) {
1387 21         179 ( $year, $s1, $month, $s2, $day ) = ( $2, $3, $4, $5, $6 );
1388             }
1389              
1390              
1391 106 100 100     14275 if ( $allow_2_digit_years && ! defined $year ) {
1392             # Unknown format, use hint to decide ...
1393 19 100       172 if ( $in_date =~ m/(^|\D)(\d{6})($|\D)/ ) {
    100          
1394 3         14 ( $year, $month, $day ) = parse_6_digit_date ( $2, $date_format_options );
1395 3         881 $s1 = $s2 = "";
1396              
1397             # Unknown format, use hint to decide ...
1398             } elsif ( $in_date =~ m/(^|[^:\d])(\d{1,2})([^:\d]+)(\d{1,2})([^:\d]+)(\d{1,2})([^:\d]|$)/ ) {
1399 14         78 ( $s1, $s2 ) = ( $3, $5 );
1400 14         117 my $date = sprintf ("%02d%02d%02d", $2, $4, $6);
1401 14         63 ( $year, $month, $day ) = parse_6_digit_date ( $date, $date_format_options );
1402             }
1403             } # End if allowing 2-digit years ...
1404              
1405 106 100       4578 if ( defined $year ) {
1406 92         322 return DBUG_RETURN ( _check_if_good_date ($in_date, $year, $month, $day) );
1407             }
1408              
1409 14         61 DBUG_RETURN ( undef );
1410             }
1411              
1412              
1413             # --------------------------------------------------------------
1414             # Always returns date in ISO format if it's good!
1415             # Or undef if a bad date!
1416              
1417             sub _check_if_good_date
1418             {
1419 2817     2817   12028 DBUG_ENTER_FUNC ( @_ );
1420 2817         685320 my $in_str = shift;
1421 2817         5717 my $year = shift;
1422 2817         6913 my $month = shift;
1423 2817         5428 my $day = shift;
1424              
1425             # Strip off any leading zeros so we can use the hashes for validation ...
1426 2817         8067 $month =~ s/^0+//;
1427 2817         7413 $day =~ s/^0+//;
1428              
1429             # Standardize it ... (with digits only!)
1430 2817         8672 $month = $Months{lcx($month)};
1431 2817         7104 $day = $Days{lcx($day)};
1432              
1433             # Helpfull when dealing with foreign languages.
1434 2817         5796 my $err_msg;
1435 2817 50 33     15000 if ( defined $month && defined $day ) {
    0          
    0          
1436             ; # Good date!
1437             } elsif ( defined $month ) {
1438 0         0 $err_msg = "Just the day of month is bad.";
1439             } elsif ( defined $day ) {
1440 0         0 $err_msg = "Just the month is bad.";
1441             } else {
1442 0         0 $err_msg = "Both the month and day are bad.";
1443             }
1444              
1445 2817 50       7295 unless ( $err_msg ) {
1446 2817 50 33     15709 if ( 1 <= $day && $day <= $days_in_months[$month] ) {
    0 0        
1447             ; # It's a good date ...
1448             } elsif ( $month == 2 && $day == 29 ) {
1449 0         0 my $leap = _is_leap_year ($year);
1450 0 0       0 $year = undef unless ( $leap );
1451             } else {
1452 0         0 $year = undef;
1453             }
1454 2817 50       9346 unless ( defined $year ) {
1455 0         0 $err_msg = "The day of month is out of range.";
1456             }
1457             }
1458              
1459 2817 50       7178 if ( $err_msg ) {
1460 0         0 DBUG_PRINT ("ERROR", "'%s' was an invalid date!\n%s", $in_str, $err_msg);
1461 0         0 DBUG_PRINT ("BAD", "%s-%s-%s", $year, $month, $day);
1462 0         0 return ( DBUG_RETURN (undef) );
1463             }
1464              
1465 2817         21050 DBUG_RETURN ( sprintf ("%04d-%02d-%02d", $year, $month, $day) );
1466             }
1467              
1468             # --------------------------------------------------------------
1469             sub _find_month_in_string
1470             {
1471 2833     2833   9989 DBUG_ENTER_FUNC (@_);
1472 2833         633657 my $date_str = shift;
1473              
1474 2833         5776 my $month;
1475 2833         6252 my $digits = 0;
1476              
1477 2833 50       52750 my @lst = sort { length($b) <=> length($a) || $a cmp $b } keys %Months;
  385192         740237  
1478              
1479 2833         17250 foreach my $m ( @lst ) {
1480             # Ignore numeric keys, can't get the correct one from string ...
1481 36431 100       101843 next if ( $m =~ m/^\d+$/ );
1482              
1483             my $flag1 = ( $last_language_edit_flags{month_period} &&
1484 34763   33     79118 $m =~ s/[.]/\\./g );
1485              
1486 34763 100       404000 if ( $date_str =~ m/(${m})/ ) {
1487 2727         9622 $month = $1;
1488 2727 50       10362 $month =~ s/[.]/\\./g if ( $flag1 );
1489 2727         6889 last;
1490             }
1491             }
1492              
1493             # Allow any number between 1 and 12 ...
1494 2833 100       8779 unless ( $month ) {
1495 106         227 $month = "[1-9]|0[1-9]|1[0-2]";
1496 106         201 $digits = 1;
1497             }
1498              
1499 2833         13186 DBUG_RETURN ( $month, $digits ); # Suitable for use in a RegExpr.
1500             }
1501              
1502             # --------------------------------------------------------------
1503             sub _find_day_of_month_in_string
1504             {
1505 2833     2833   11428 DBUG_ENTER_FUNC (@_);
1506 2833         672736 my $date_str = shift;
1507 2833         6364 my $skip_period = shift; # Skip entries ending in '.' like 17.!
1508 2833         6555 my $month_str = shift; # Will be undef if skip_period is true!
1509              
1510 2833         4816 my $day;
1511 2833         5673 my $digits = 0;
1512              
1513 2833 50       58109 my @lst = sort { length($b) <=> length($a) || $a cmp $b } keys %Days;
  703617         1306242  
1514              
1515 2833 100       20539 my $all_digits = $skip_period ? "^\\d+[.]?\$" : "^\\d+\$";
1516              
1517 2833         9303 foreach my $dom ( @lst ) {
1518             # Ignore numeric keys, can't get the correct one from string ...
1519 116676 100       380690 next if ( $dom =~ m/${all_digits}/ );
1520              
1521             my $flag1 = ( $last_language_edit_flags{dsuf_period} &&
1522 53788   66     126625 $dom =~ s/[.]/\\./g );
1523              
1524 53788 100       117179 if ( $month_str ) {
    50          
1525             # Makes sure dom doesn't match month name ...
1526 50502         90022 $month_str =~ s/[.]/\\./g;
1527 50502 100 66     1130418 if ( $date_str =~ m/${month_str}.*(${dom})/ ||
1528             $date_str =~ m/(${dom}).*${month_str}/ ) {
1529 941         3607 $day = $1;
1530 941 100       2864 $day =~ s/[.]/\\./g if ( $flag1 );
1531 941         2917 last;
1532             }
1533              
1534             # There is no month name to worry about ...
1535             } elsif ( $date_str =~ m/(${dom})/ ) {
1536 0         0 $day = $1;
1537 0 0       0 $day =~ s/[.]/\\./g if ( $flag1 );
1538 0         0 last;
1539             }
1540             }
1541              
1542             # Allow any number between 1 and 31 ...
1543 2833 100       8914 unless ( $day ) {
1544 1892         4237 $day = "[1-9]|0[1-9]|[12][0-9]|3[01]";
1545 1892         4521 $digits = 1;
1546             }
1547              
1548 2833         12591 DBUG_RETURN ( $day, $digits ); # Suitable for use in a RegExpr.
1549             }
1550              
1551             # ==============================================================
1552              
1553             =item adjust_future_cutoff ( $num_years );
1554              
1555             Changes the cutoff future date from B<30> years to I<$num_years>.
1556              
1557             Set to B<0> to disable years in the future!
1558              
1559             This affects all L objects, not just the current one.
1560              
1561             =cut
1562              
1563             sub adjust_future_cutoff
1564             {
1565 0     0 1 0 DBUG_ENTER_FUNC ( @_ );
1566 0         0 my $years = shift;
1567              
1568 0 0 0     0 if ( defined $years && $years =~ m/^\d+$/ ) {
1569 0         0 $global_cutoff_date = shift;
1570             }
1571              
1572 0         0 DBUG_VOID_RETURN ();
1573             }
1574              
1575              
1576             # ==============================================================
1577              
1578             =item $year = make_it_a_4_digit_year ( $two_digit_year );
1579              
1580             Used whenever this module needs to convert a two-digit year into a four-digit
1581             year.
1582              
1583             When it converts YY into YYYY, it will assume 20YY unless the
1584             resulting date is more than B<30> years in the future. Then it's 19YY.
1585              
1586             If you don't like this rule, use B to change
1587             this limit!
1588              
1589             =cut
1590              
1591             sub make_it_a_4_digit_year
1592             {
1593 1324     1324 1 6423 DBUG_ENTER_FUNC ( @_ );
1594 1324   50     311750 my $year = shift || 0; # Passed as a 2-digit year ...
1595              
1596 1324         4433 $year += 2000; # Convert it to a 4-digit year ...
1597              
1598             # Get the current 4-digit year ...
1599 1324         52750 my $this_yr = (localtime (time()))[5];
1600 1324         4041 $this_yr += 1900;
1601              
1602 1324 100 66     8071 if ( $this_yr < $year && ($year - $this_yr) >= $global_cutoff_date ) {
1603 1293         2947 $year -= 100; # Make it last century instead.
1604             }
1605              
1606 1324         5014 DBUG_RETURN ( $year );
1607             }
1608              
1609              
1610             # ==============================================================
1611              
1612             =item ($year, $month, $day) = parse_8_digit_date ( $date_str, $order[, $skip] );
1613              
1614             Looks for a valid date in an 8 digit string. It checks each of the formats below
1615             in the order specified by I<$order> until it hits something that looks like a
1616             valid date.
1617              
1618             (1) YYYYMMDD - ISO
1619             (2) MMDDYYYY - American
1620             (3) DDMMYYYY - European
1621              
1622             The I<$order> argument helps deal with ambiguities in the date. Its a comma
1623             separated list of numbers specifying to order to try out. Ex: 3,2,1 means
1624             try out the European date format 1st, then the American date format 2nd, and
1625             finally the ISO format 3rd. You could also just say I<$order> is B<3> and
1626             only accept European dates.
1627              
1628             It assumes its using the correct format when the date looks valid. It does this
1629             by validating the B is between 1 and 12 and that the B
is between 1 and
1630             31. (Using the correct max for that month). And then assumes the year is
1631             always valid.
1632              
1633             If I<$skip> is a non-zero value it will skip over the B format if it's
1634             listed in I<$order>.
1635              
1636             Returns 3 B's if nothing looks good.
1637              
1638             =cut
1639              
1640             sub parse_8_digit_date
1641             {
1642 54     54 1 202 DBUG_ENTER_FUNC ( @_ );
1643 54         43433 my $date_str = shift;
1644 54         166 my $order = shift;
1645 54   100     221 my $skip_iso = shift || 0;
1646              
1647 54         447 my @order = split (/\s*,\s*/, $order);
1648 54         195 my @lbls = ( "", "YYYYMMDD - ISO", "MMDDYYYY - American", "DDMMYYYY - European" );
1649              
1650 54         112 my ( $year, $month, $day );
1651 54         158 foreach my $id ( @order ) {
1652 101 50 33     638 next unless ( defined $id && $id =~ m/^[123]$/ );
1653              
1654 101         239 my ( $y, $m, $d ) = ( 0, 0, 0 );
1655              
1656 101 100 100     503 if ( $id == 1 && (! $skip_iso) && # YYYYMMDD - ISO
      66        
1657             $date_str =~ m/^(\d{4})(\d{2})(\d{2})$/ ) {
1658 12         55 ( $y, $m, $d ) = ( $1, $2, $3 );
1659             }
1660 101 100 66     476 if ( $id == 2 && # MMDDYYYY - American
1661             $date_str =~ m/^(\d{2})(\d{2})(\d{4})$/ ) {
1662 47         249 ( $m, $d, $y ) = ( $1, $2, $3 );
1663             }
1664 101 50 33     271 if ( $id == 3 && # DDMMYYYY - European
1665             $date_str =~ m/^(\d{2})(\d{2})(\d{4})$/ ) {
1666 0         0 ( $d, $m, $y ) = ( $1, $2, $3 );
1667             }
1668              
1669 101 50 100     646 if ( 1 <= $m && $m <= 12 && 1 <= $d && $d <= 31 ) {
      66        
      66        
1670 54         274 DBUG_PRINT ("INFO", "Validating if using %s format.", $lbls[$id]);
1671 54         13544 my $max = $days_in_months[$m];
1672 54 100       216 if ( $m == 2 ) {
1673 3         15 my $leap = _is_leap_year ($y);
1674 3 50       10 ++$max if ( $leap );
1675             }
1676              
1677 54 50       327 if ( $d <= $max ) {
1678 54         196 ( $year, $month, $day ) = ( $y, $m, $d );
1679 54         126 last;
1680             }
1681             }
1682             }
1683              
1684 54         185 DBUG_RETURN ( $year, $month, $day );
1685             }
1686              
1687              
1688             # ==============================================================
1689              
1690             =item ($year, $month, $day) = parse_6_digit_date ( $date_str, $order );
1691              
1692             Looks for a valid date in an 6 digit string. It checks each of the formats below
1693             in the order specified by I<$order> until it hits something that looks like a
1694             valid date.
1695              
1696             (1) YYMMDD - ISO
1697             (2) MMDDYY - American
1698             (3) DDMMYY - European
1699              
1700             The I<$order> argument helps deal with ambiguities in the date. Its a comma
1701             separated list of numbers specifying to order to try out. Ex: 2,3,1 means
1702             try out the American date format 1st, then the European date format 2nd, and
1703             finally the ISO format 3rd. You could also just say I<$order> is B<2> and
1704             only accept European dates.
1705              
1706             So if you use the wrong order, more than likely you'll get the wrong date!
1707              
1708             It assumes its using the correct format when the date looks valid. It does this
1709             by validating the B is between 1 and 12 and that the B
is between 1 and
1710             31. (Using the correct max for that month). And then assumes the year is
1711             always valid.
1712              
1713             Returns 3 B's if nothing looks good.
1714              
1715             It always returns the year as a 4-digit year!
1716              
1717             =cut
1718              
1719             sub parse_6_digit_date
1720             {
1721 26     26 1 114 DBUG_ENTER_FUNC ( @_ );
1722 26         15681 my $date_str = shift;
1723 26         69 my $order = shift;
1724              
1725 26         221 my @order = split (/\s*,\s*/, $order);
1726 26         107 my @lbls = ( "", "YYMMDD - ISO", "MMDDYY - American", "DDMMYY - European" );
1727              
1728 26         63 my ( $year, $month, $day );
1729 26 50       192 if ( $date_str =~ m/^(\d{2})(\d{2})(\d{2})$/ ) {
1730 26         135 my @part = ( $1, $2, $3 );
1731 26         74 foreach my $id ( @order ) {
1732 29 50 33     226 next unless ( defined $id && $id =~ m/^[123]$/ );
1733              
1734 29         87 my ( $y, $m, $d ) = ( 0, 0, 0 );
1735              
1736 29 100 66     361 if ( $id == 1 && # YYMMDD - ISO
      66        
      33        
      66        
1737             1 <= $part[1] && $part[1] <= 12 &&
1738             1 <= $part[2] && $part[2] <= 31 ) {
1739 23         97 ( $m, $d, $y ) = ( $part[1], $part[2], $part[0] );
1740             }
1741 29 50 66     115 if ( $id == 2 && # MMDDYY - American
      66        
      33        
      33        
1742             1 <= $part[0] && $part[0] <= 12 &&
1743             1 <= $part[1] && $part[1] <= 31 ) {
1744 3         10 ( $m, $d, $y ) = ( $part[0], $part[1], $part[2] );
1745             }
1746 29 0 33     90 if ( $id == 3 && # DDMMYY - European
      33        
      0        
      0        
1747             1 <= $part[1] && $part[1] <= 12 &&
1748             1 <= $part[0] && $part[0] <= 31 ) {
1749 0         0 ( $m, $d, $y ) = ( $part[1], $part[0], $part[2] );
1750             }
1751              
1752             # Now validate the day of month ...
1753 29 100       80 if ( $m > 0 ) {
1754 26         110 DBUG_PRINT ("INFO", "Validating if using %s format.", $lbls[$id]);
1755 26         6780 $y = make_it_a_4_digit_year ( $y );
1756              
1757 26         19713 my $max = $days_in_months[$m];
1758 26 50       91 if ( $m == 2 ) {
1759 0         0 my $leap = _is_leap_year ($y);
1760 0 0       0 ++$max if ( $leap );
1761             }
1762              
1763 26 50       93 if ( $d <= $max ) {
1764 26         91 ( $year, $month, $day ) = ( $y, $m, $d );
1765 26         110 last;
1766             }
1767             }
1768             }
1769             }
1770              
1771 26         79 DBUG_RETURN ( $year, $month, $day );
1772             }
1773              
1774              
1775             # ==============================================================
1776              
1777             =item (\@months, \@weekdays) = init_special_date_arrays ( $lang[, $mode[, $wok[, $wide]]] );
1778              
1779             Prefers getting the date names from I for the
1780             I special date variables. But if the language isn't supported
1781             by that module it tries I instead. This is because
1782             the 1st module is more consistent.
1783              
1784             If the I<$lang> doesn't exist, then it returns the arrays for the last valid
1785             language.
1786              
1787             If I<$wok> is set to a non-zero value, it will print warnings to your screen if
1788             there were issues in changing the language used.
1789              
1790             I<$mode> tells how to return the various arrays:
1791              
1792             1 - Abbreviated month/weekday names in the requested language.
1793             2 - Full month/weekday names in the requested language.
1794             Any other value and it will return the numeric values. (default)
1795              
1796             For @months, indexes are 0..11, with 0 representing January.
1797              
1798             For @weekdays, indexes are 0..6, with 0 representing Sunday.
1799              
1800             Languages like 'Greek' that rely on I require the I<$wide> flag set to
1801             true. Otherwise that language is disabled.
1802              
1803             =cut
1804              
1805             sub init_special_date_arrays
1806             {
1807 148     148 1 13007 DBUG_ENTER_FUNC ( @_ );
1808 148         79269 my $lang = shift;
1809 148   100     986 my $mode = shift || 0; # Default to numeric arrays ...
1810 148   100     764 my $warn_ok = shift || 0;
1811 148   50     733 my $allow_wide = shift || 0;
1812              
1813 148         1064 my @months = ( "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12" );
1814 148         689 my @week_days = ( "1", "2", "3", "4", "5", "6", "7" );
1815              
1816 148   100     727 my $numbers = ($mode != 1 && $mode != 2 );
1817              
1818 148         354 my ( $lang_ref, $manip_ref );
1819              
1820 148 50       561 if ( defined $lang ) {
1821 148         835 ($manip_ref, $lang_ref) = _select_language ($lang, $warn_ok, $allow_wide);
1822              
1823 148 50 33     39992 unless ( $lang_ref || $manip_ref ) {
1824 0         0 $lang = undef; # So it will enter the early out if block ...
1825             }
1826             }
1827              
1828 148 100 66     1265 if ( (! defined $lang) || lc($lang) eq lc($prev_array_lang) || $numbers ) {
      100        
1829 146 100       815 if ( $mode == 1 ) {
    100          
1830 3         70 @months = @gMoYs; # Abrevited month names ...
1831 3         24 @week_days = @gDoWs; # Abrevited week names ...
1832             } elsif ( $mode == 2 ) {
1833 7         64 @months = @gMoY; # Full month names ...
1834 7         38 @week_days = @gDoW; # Full week names ...
1835             }
1836 146         668 return DBUG_RETURN ( \@months, \@week_days );
1837             }
1838              
1839 2         5 my ($MoY_ref, $MoYs_ref, $Dsuf_ref, $DoW_ref, $DoWs_ref);
1840              
1841 2         9 DBUG_PRINT ("INFO", "Manip: %s, Lang: %s", $manip_ref, $lang_ref);
1842 2 50       357 if ( $manip_ref ) {
1843 0         0 my ( $u1, $u2, $u3 ); # Unused placeholders.
1844 0         0 ($u1, $u2, $u3, $MoY_ref, $MoYs_ref, $Dsuf_ref, $DoW_ref, $DoWs_ref) =
1845             _swap_manip_language_common ($manip_ref, $warn_ok, $allow_wide );
1846 0         0 $lang = $manip_ref->{Language};
1847              
1848 0 0       0 if ( $u1 ) {
1849 0         0 $lang_ref = undef; # Skip lang_ref lookup if successsful ...
1850             } else {
1851 0         0 $lang_ref = $date_language_installed_languages{lc($lang)};
1852             }
1853             }
1854              
1855 2 50       6 if ( $lang_ref ) {
1856 2         8 ($MoY_ref, $MoYs_ref, $Dsuf_ref, $DoW_ref, $DoWs_ref) =
1857             _swap_lang_common ( $lang_ref, $warn_ok, $allow_wide );
1858 2         563 $lang = $lang_ref->{Language};
1859             }
1860              
1861              
1862             # If the new language was valid, update the global variables ...
1863 2 50       11 if ( $MoY_ref ) {
1864 2         6 $prev_array_lang = $lang;
1865 2         4 @gMoY = @{$MoY_ref};
  2         17  
1866 2         5 @gMoYs = map { uc($_) } @{$MoYs_ref};
  24         46  
  2         6  
1867 2         5 @gDoW = @{$DoW_ref};
  2         23  
1868 2         4 @gDoWs = map { uc($_) } @{$DoWs_ref};
  14         53  
  2         6  
1869 2         4 @gDsuf = @{$Dsuf_ref};
  2         26  
1870              
1871 2         46 DBUG_PRINT ( "LANGUAGE", "%s\n%s\n%s\n%s\n%s",
1872             join (", ", @gMoY), join (", ", @gMoYs),
1873             join (", ", @gDoW), join (", ", @gDoWs),
1874             join (", ", @gDsuf)
1875             );
1876             }
1877              
1878             # Numeric handled earlier ...
1879 2 50       587 if ( $mode == 1 ) {
    0          
1880 2         14 @months = @gMoYs; # Abrevited month names ...
1881 2         10 @week_days = @gDoWs; # Abrevited week names ...
1882             } elsif ( $mode == 2 ) {
1883 0         0 @months = @gMoY; # Full month names ...
1884 0         0 @week_days = @gDoW; # Full week names ...
1885             }
1886              
1887 2         8 DBUG_RETURN ( \@months, \@week_days );
1888             }
1889              
1890             # ==============================================================
1891              
1892             sub _is_leap_year
1893             {
1894 26500     26500   35950 my $year = shift;
1895 26500   100     60463 my $leap = ($year % 4 == 0) && ($year % 100 != 0 || $year % 400 == 0);
1896 26500 100       48621 return ($leap ? 1 : 0);
1897             }
1898              
1899             # ==============================================================
1900              
1901             # Validate the input date.
1902             sub _validate_date_str
1903             {
1904 828     828   2618 DBUG_ENTER_FUNC ( @_ );
1905 828         439257 my $date_str = shift;
1906              
1907 828         2010 my ($year, $mon, $day);
1908 828 100 66     8271 if ( defined $date_str && $date_str =~ m/^(\d+)-(\d+)-(\d+)$/ ) {
1909 824         4427 ($year, $mon, $day) = ($1, $2, $3);
1910 824         2044 my $leap = _is_leap_year ($year);
1911 824 100       2594 local $days_in_months[2] = $leap ? 29 : 28;
1912 824 50 33     6067 unless ( 1 <= $mon && $mon <= 12 &&
      33        
      33        
1913             1 <= $day && $day <= $days_in_months[$mon] ) {
1914 0         0 return DBUG_RETURN ( undef, undef, undef );
1915             }
1916             } else {
1917 4         18 return DBUG_RETURN ( undef, undef, undef );
1918             }
1919              
1920 824         2666 DBUG_RETURN ( $year, $mon, $day );
1921             }
1922              
1923             # ==============================================================
1924              
1925             =item $bool = is_leap_year ( $year );
1926              
1927             Returns B<1> if I<$year> is a Leap Year, else B<0> if it isn't.
1928              
1929             =cut
1930              
1931             sub is_leap_year
1932             {
1933 134     134 1 40763 DBUG_ENTER_FUNC ( @_ );
1934 134         78128 DBUG_RETURN ( _is_leap_year (@_) );
1935             }
1936              
1937             # ==============================================================
1938              
1939             =item $hyd = calc_hundred_year_date ( $date_str );
1940              
1941             Takes a date string in B format and returns the number of days since
1942             B<1899-12-31>. (Which is HYD B<0>.) It should be compatible with DB2's data
1943             type of the same name. Something like this function is needed if you wish to be
1944             able to do date math.
1945              
1946             For example:
1947              
1948             1 : 2026-01-01 - 2025-12-30 = 2 days.
1949             2 : 2025-12-31 + 10 = 2026-01-10.
1950             2 : 2025-12-31 - 2 = 2025-12-29.
1951              
1952             If the given date string is invalid it will return B.
1953              
1954             =cut
1955              
1956             sub calc_hundred_year_date
1957             {
1958 184     184 1 108375 DBUG_ENTER_FUNC ( @_ );
1959 184         101999 my $date_str = shift;
1960              
1961             # Validate the input date.
1962 184         577 my ($end_year, $month, $day) = _validate_date_str ($date_str);
1963 184 50       48420 unless (defined $end_year) {
1964 0         0 return DBUG_RETURN ( undef );
1965             }
1966              
1967 184         395 my $hyd = 0;
1968 184         340 my $start_year = 1899;
1969              
1970 184 100       682 if ( $end_year > $start_year ) {
1971 96         423 for (my $year = $start_year + 1; $year < $end_year; ++$year) {
1972 10318         15811 my $leap = _is_leap_year ($year);
1973 10318 100       22183 $hyd += $leap ? 366 : 365;
1974             }
1975 96         298 $hyd += calc_day_of_year ($date_str, 0);
1976              
1977             } else { # $hyd <= 0 ...
1978 88         338 for (my $year = $start_year; $year > $end_year; --$year) {
1979 6226         9699 my $leap = _is_leap_year ($year);
1980 6226 100       12845 $hyd -= $leap ? 366 : 365;
1981             }
1982 88         262 $hyd -= calc_day_of_year ($date_str, 1);
1983             }
1984              
1985 184         47183 DBUG_RETURN ($hyd);
1986             }
1987              
1988             # ==============================================================
1989              
1990             =item $dow = calc_day_of_week ( $date_str );
1991              
1992             Takes a date string in B format and returns the day of the week it
1993             falls on. It returns a value between B<0> and B<6> for Sunday to Saturday.
1994              
1995             If the given date is invalid it will return B.
1996              
1997             =item $dow = calc_day_of_week ( $hyd );
1998              
1999             It takes an integer as a Hundred Year Date and returns the day of the week it
2000             falls on. It returns a value between B<0> and B<6> for Sunday to Saturday.
2001              
2002             If the given hyd is not an integer it will return B.
2003              
2004             =cut
2005              
2006             sub calc_day_of_week
2007             {
2008 181     181 1 284420 DBUG_ENTER_FUNC ( @_ );
2009 181         87641 my $date_str = shift; # or a HYD ...
2010              
2011 181         464 my $hyd;
2012 181 100 66     1901 if ( defined $date_str && $date_str =~ m/^[-]?\d+$/ ) {
2013 89         262 $hyd = $date_str;
2014             } else {
2015 92         275 $hyd = calc_hundred_year_date ( $date_str );
2016             }
2017              
2018 181 50       20326 unless (defined $hyd) {
2019 0         0 return DBUG_RETURN ( undef );
2020             }
2021              
2022 181         320 my $start_dow = 0; # $hyd 0, 1899-12-31, falls on a Sunday.
2023              
2024 181         425 my $dow = ($hyd + $start_dow) % 7;
2025              
2026 181         517 DBUG_RETURN ($dow);
2027             }
2028              
2029             # ==============================================================
2030              
2031             =item $date_str = convert_hyd_to_date_str ( $hyd );
2032              
2033             It takes an integer as a Hundred Year Date and converts it into a date string
2034             in the format of B and returns it.
2035              
2036             If the given hyd is not an integer it will return B.
2037              
2038             =cut
2039              
2040             sub convert_hyd_to_date_str
2041             {
2042 89     89 1 141413 DBUG_ENTER_FUNC ( @_ );
2043 89         43454 my $target_hyd = shift;
2044              
2045 89 50 33     1022 unless ( defined $target_hyd && $target_hyd =~ m/^[-]?\d+$/ ) {
2046 0         0 return DBUG_RETURN ( undef );
2047             }
2048              
2049 89         222 my $date_str;
2050 89         202 my $start_year = 1899; # HYD of 0 is 1899-12-31
2051 89         170 my $hyd_total = 0;
2052 89         200 my $days = 0;
2053 89         218 my ($leap, $year);
2054              
2055 89 100       311 if ( $target_hyd > 0 ) {
2056 45         144 for ($year = $start_year + 1; 1==1; ++$year) {
2057 5204         7911 $leap = _is_leap_year ($year);
2058 5204 100       8066 $days = $leap ? 366 : 365;
2059 5204 100       9033 if ( ($hyd_total + $days) >= $target_hyd ) {
2060 45         141 last;
2061             }
2062 5159         6995 $hyd_total += $days;
2063             }
2064 45 100       206 local $days_in_months[2] = $leap ? 29 : 28;
2065 45         162 for (1..12) {
2066 274         427 $days = $days_in_months[$_];
2067 274 100       533 if ( ($hyd_total + $days) >= $target_hyd ) {
2068 45         86 my $diff = $target_hyd - $hyd_total;
2069 45         234 $date_str = sprintf ("%04d-%02d-%02d", $year, $_, $diff);
2070 45         127 last;
2071             }
2072 229         366 $hyd_total += $days;
2073             }
2074              
2075             } else { # $target_hyd <= 0.
2076 44         106 for ($year = $start_year; 1==1; --$year) {
2077 3155         4459 $leap = _is_leap_year ($year);
2078 3155 100       5117 $days = $leap ? 366 : 365;
2079 3155 100       5371 if ( ($hyd_total - $days) <= $target_hyd ) {
2080 44         119 last;
2081             }
2082 3111         3934 $hyd_total -= $days;
2083             }
2084 44 100       176 local $days_in_months[2] = $leap ? 29 : 28;
2085 44         151 for (reverse 1..12) {
2086 292         55938 $days = $days_in_months[$_];
2087 292 100       851 if ( ($hyd_total - $days) <= $target_hyd ) {
2088 44         92 my $diff = $target_hyd - $hyd_total;
2089 44         116 my $ans = $diff + $days;
2090              
2091 44         185 DBUG_PRINT("-FINAL-", "Target: %d, Current: %d, Diff: %d, Year: %d/%02d, Day: %02d", $target_hyd, $hyd_total, $diff, $year, $_, $ans);
2092              
2093 44 100       10166 if ($ans) {
    100          
2094 29         132 $date_str = sprintf ("%04d-%02d-%02d", $year, $_, $ans);
2095             } elsif ( $_ == 1 ) {
2096 2         6 $ans = $days_in_months[12];
2097 2         13 $date_str = sprintf ("%04d-%02d-%02d", $year - 1, 12, $ans);
2098             } else {
2099 13         37 $ans = $days_in_months[$_ - 1];
2100 13         60 $date_str = sprintf ("%04d-%02d-%02d", $year, $_ - 1, $ans);
2101             }
2102 44         136 last;
2103             }
2104 248         442 $hyd_total -= $days;
2105              
2106 248         736 DBUG_PRINT("MONTHLY", "Target: %d, Current: %d, Year: %d/%02d", $target_hyd, $hyd_total, $year, $_);
2107             }
2108             }
2109              
2110 89         348 DBUG_RETURN ($date_str);
2111             }
2112              
2113             # ==============================================================
2114              
2115             =item $doy = calc_day_of_year ( $date_str[, $remainder_flag] );
2116              
2117             Takes a date string in B format and returns the number of days since
2118             the begining of the year. With January 1st being day B<1>.
2119              
2120             If the remainder_flag is set to a no-zero value, it returns the number of days
2121             left in the year. With December 31st being B<0>.
2122              
2123             If the given date is invalid it will return B.
2124              
2125             =cut
2126              
2127             sub calc_day_of_year
2128             {
2129 540     540 1 520237 DBUG_ENTER_FUNC ( @_ );
2130 540         282653 my $date_str = shift;
2131 540   100     2270 my $remainder_flag = shift || 0;
2132              
2133             # Validate the input date.
2134 540         1386 my ($year, $month, $day) = _validate_date_str ($date_str);
2135 540 50       146775 unless (defined $year) {
2136 0         0 return DBUG_RETURN ( undef );
2137             }
2138              
2139 540         1319 my $leap = _is_leap_year ($year);
2140 540 100       1707 local $days_in_months[2] = $leap ? 29 : 28;
2141              
2142 540         1052 my $doy = 0;
2143 540         1477 for (my $m = 0; $m < $month; ++$m) {
2144 4372         9480 $doy += $days_in_months[$m];
2145             }
2146 540         918 $doy += $day;
2147              
2148 540 100       1333 if ($remainder_flag) {
2149 264 100       768 my $total_days_in_year = $leap ? 366 : 365;
2150 264         464 $doy = $total_days_in_year - $doy;
2151             }
2152              
2153 540         1573 DBUG_RETURN ($doy);
2154             }
2155              
2156             # ==============================================================
2157              
2158             =item $date_str = adjust_date_str ( $date_str, $years, $months );
2159              
2160             Takes a date string in B format and adjusts it by the given number
2161             of months and years. It returns the new date in B format.
2162              
2163             It does its best to preserve the day of month, but if it would exceed the number
2164             of days in a month, it will truncate to the end of month. Not round to the next
2165             month.
2166              
2167             Returns I if passed bad arguments.
2168              
2169             =cut
2170              
2171             sub adjust_date_str
2172             {
2173 96     96 1 137413 DBUG_ENTER_FUNC ( @_ );
2174 96         48170 my $date_str = shift;
2175 96   100     475 my $adj_years = shift || 0;
2176 96   100     315 my $adj_months = shift || 0;
2177              
2178             # Validate the input date.
2179 96         270 my ($year, $month, $day) = _validate_date_str ($date_str);
2180 96 50 33     25555 unless (defined $year &&
      33        
2181             $adj_years =~ m/^[-]?\d+$/ && $adj_months =~ m/^[-]?\d+$/) {
2182 0         0 return DBUG_RETURN ( undef );
2183             }
2184              
2185             # Adjust by month ...
2186 96 100       285 if ( $adj_months >= 0 ) {
2187 65         202 foreach (1..${adj_months}) {
2188 284 100       428 if ( $month == 12 ) {
2189 37         61 $month = 1;
2190 37         67 ++$adj_years;
2191             } else {
2192 247         359 ++$month;
2193             }
2194             }
2195             } else {
2196 31         126 foreach (1..-${adj_months}) {
2197 358 100       676 if ( $month == 1 ) {
2198 17         32 $month = 12;
2199 17         35 --$adj_years;
2200             } else {
2201 341         600 --$month;
2202             }
2203             }
2204             }
2205              
2206             # Adjust the years ...
2207 96         211 $year += $adj_years;
2208              
2209             # Build the returned date ...
2210 96         218 my $leap = _is_leap_year ($year);
2211 96 100       276 local $days_in_months[2] = $leap ? 29 : 28;
2212 96         209 my $d = $days_in_months[$month];
2213              
2214 96 100       489 $date_str = sprintf ("%04d-%02d-%02d", $year, $month,
2215             ($day <= $d) ? $day : $d);
2216              
2217 96         285 DBUG_RETURN ($date_str);
2218             }
2219              
2220             # ==============================================================
2221              
2222             =back
2223              
2224             =head1 SOME EXAMPLE DATES
2225              
2226             Here are some sample date strings in B that this module can parse.
2227             All for Christmas 2017. This is not a complete list of available date formats
2228             supported. But should hopefully give you a starting point of what is possible.
2229             Remember that if a date string contains extra info around the date part of it,
2230             that extra information is thrown away.
2231              
2232             S<12/25/2017>, B>, S, B>,
2233             S, B>, S<25-DEC-2017>,
2234             B>, S<20171225>, B>,
2235             S, B>.
2236              
2237             Most of the above examples will also work with 2-digit years as well.
2238              
2239             And just to remind you that other languages are supported if L
2240             is installed, here's a date in Spanish that would be legal after
2241             S was called.
2242              
2243             =over 4
2244              
2245             B>.
2246              
2247             =back
2248              
2249             =head1 COPYRIGHT
2250              
2251             Copyright (c) 2018 - 2026 Curtis Leach. All rights reserved.
2252              
2253             This program is free software. You can redistribute it and/or modify it under
2254             the same terms as Perl itself.
2255              
2256             =head1 SEE ALSO
2257              
2258             L - The main user of this module. It defines the Config object.
2259              
2260             L - Handles the configuration of the Config module.
2261              
2262             L - Handles the parsing of the config file.
2263              
2264             L - Provides some sample config files and commentary.
2265              
2266             L - Provides foreign language support.
2267              
2268             L - Provides additional foreign language support.
2269              
2270             =cut
2271              
2272             # ==============================================================
2273             #required if module is included w/ require command;
2274             1;
2275