source: gs2-extensions/parallel-building/trunk/src/perllib/cpan/Image/ExifTool/Shift.pl@ 24626

Last change on this file since 24626 was 24626, checked in by jmt12, 13 years ago

An (almost) complete copy of the perllib directory from a (circa SEP2011) head checkout from Greenstone 2 trunk - in order to try and make merging in this extension a little easier later on (as there have been some major changes to buildcol.pl commited in the main trunk but not in the x64 branch)

File size: 21.5 KB
Line 
1#------------------------------------------------------------------------------
2# File: Shift.pl
3#
4# Description: ExifTool time shifting routines
5#
6# Revisions: 10/28/2005 - P. Harvey Created
7#------------------------------------------------------------------------------
8
9package Image::ExifTool;
10
11use strict;
12
13sub ShiftTime($$$;$);
14
15#------------------------------------------------------------------------------
16# apply shift to value in new value hash
17# Inputs: 0) shift type, 1) shift string, 2) raw date/time value, 3) new value hash ref
18# Returns: error string or undef on success and updates value in new value hash
19sub ApplyShift($$$;$)
20{
21 my ($func, $shift, $val, $nvHash) = @_;
22
23 # get shift direction from first character in shift string
24 $shift =~ s/^(\+|-)// or return 'Bad shift string (no sign)';
25 my $pre = $1;
26 my $dir = ($pre eq '+') ? 1 : -1;
27 my $tagInfo = $$nvHash{TagInfo};
28 my $tag = $$tagInfo{Name};
29 my $self = $$nvHash{Self}; # (used in eval)
30 my $shiftOffset;
31 if ($$nvHash{ShiftOffset}) {
32 $shiftOffset = $$nvHash{ShiftOffset};
33 } else {
34 $shiftOffset = $$nvHash{ShiftOffset} = { };
35 }
36
37 # initialize handler for eval warnings
38 local $SIG{'__WARN__'} = \&SetWarning;
39 SetWarning(undef);
40
41 # shift is applied to ValueConv value, so we must ValueConv-Shift-ValueConvInv
42 my ($type, $err);
43 foreach $type ('ValueConv','Shift','ValueConvInv') {
44 if ($type eq 'Shift') {
45 #### eval ShiftXxx function
46 $err = eval "Shift$func(\$val, \$shift, \$dir, \$shiftOffset)";
47 } elsif ($$tagInfo{$type}) {
48 my $conv = $$tagInfo{$type};
49 if (ref $conv eq 'CODE') {
50 $val = &$conv($val, $self);
51 } else {
52 return "Can't handle $type for $tag in ApplyShift()" if ref $$tagInfo{$type};
53 #### eval ValueConv/ValueConvInv ($val, $self)
54 $val = eval $$tagInfo{$type};
55 }
56 } else {
57 next;
58 }
59 # handle errors
60 $err and return $err;
61 $@ and SetWarning($@);
62 GetWarning() and return CleanWarning();
63 }
64 # update value in new value hash
65 $nvHash->{Value} = [ $val ];
66 return undef; # success
67}
68
69#------------------------------------------------------------------------------
70# Check date/time shift
71# Inputs: 0) shift type, 1) shift string (without sign)
72# Returns: updated shift string, or undef on error (and may update shift)
73sub CheckShift($$)
74{
75 my ($type, $shift) = @_;
76 my $err;
77 if ($type eq 'Time') {
78 return "No shift direction" unless $shift =~ s/^(\+|-)//;
79 # do a test shift to validate the shift string
80 my $testTime = '2005:11:02 09:00:13.25-04:00';
81 $err = ShiftTime($testTime, $shift, $1 eq '+' ? 1 : -1);
82 } else {
83 $err = "Unknown shift type ($type)";
84 }
85 return $err;
86}
87
88#------------------------------------------------------------------------------
89# return the number of days in a month
90# Inputs: 0) month number (Jan=1, may be outside range), 1) year
91# Returns: number of days in month
92sub DaysInMonth($$)
93{
94 my ($mon, $year) = @_;
95 my @days = (31,28,31,30,31,30,31,31,30,31,30,31);
96 # adjust to the range [0,11]
97 while ($mon < 1) { $mon += 12; --$year; }
98 while ($mon > 12) { $mon -= 12; ++$year; }
99 # return standard number of days unless february on a leap year
100 return $days[$mon-1] unless $mon == 2 and not $year % 4;
101 # leap years don't occur on even centuries except every 400 years
102 return 29 if $year % 100 or not $year % 400;
103 return 28;
104}
105
106#------------------------------------------------------------------------------
107# split times into corresponding components: YYYY mm dd HH MM SS tzh tzm
108# Inputs: 0) date/time or shift string 1) reference to list for returned components
109# 2) optional reference to list of time components (if shift string)
110# Returns: true on success
111# Returned components are 0-Y, 1-M, 2-D, 3-hr, 4-min, 5-sec, 6-tzhr, 7-tzmin
112sub SplitTime($$;$)
113{
114 my ($val, $vals, $time) = @_;
115 # insert zeros if missing in shift string
116 if ($time) {
117 $val =~ s/(^|[-+:\s]):/${1}0:/g;
118 $val =~ s/:([:\s]|$)/:0$1/g;
119 }
120 # change dashes to colons in date (for XMP dates)
121 if ($val =~ s/^(\d{4})-(\d{2})-(\d{2})/$1:$2:$3/) {
122 $val =~ tr/T/ /; # change 'T' separator to ' '
123 }
124 # add space before timezone to split it into a separate word
125 $val =~ s/(\+|-)/ $1/;
126 my @words = split ' ', $val;
127 my $err = 1;
128 my @v;
129 for (;;) {
130 my $word = shift @words;
131 last unless defined $word;
132 # split word into separate numbers (allow decimal points but no signs)
133 my @vals = $word =~ /(?=\d|\.\d)\d*(?:\.\d*)?/g or last;
134 if ($word =~ /^(\+|-)/) {
135 # this is the timezone
136 (defined $v[6] or @vals > 2) and $err = 1, last;
137 my $sign = ($1 ne '-') ? 1 : -1;
138 # apply sign to both minutes and seconds
139 $v[6] = $sign * shift(@vals);
140 $v[7] = $sign * (shift(@vals) || 0);
141 } elsif ((@words and $words[0] =~ /^\d+/) or # there is a time word to follow
142 (not $time and $vals[0] =~ /^\d{3}/) or # first value is year (3 or more digits)
143 ($time and not defined $$time[3] and not defined $v[0])) # we don't have a time
144 {
145 # this is a date (must come first)
146 (@v or @vals > 3) and $err = 1, last;
147 not $time and @vals != 3 and $err = 1, last;
148 $v[2] = pop(@vals); # take day first if only one specified
149 $v[1] = pop(@vals) || 0;
150 $v[0] = pop(@vals) || 0;
151 } else {
152 # this is a time (can't come after timezone)
153 (defined $v[3] or defined $v[6] or @vals > 3) and $err = 1, last;
154 not $time and @vals != 3 and @vals != 2 and $err = 1, last;
155 $v[3] = shift(@vals); # take hour first if only one specified
156 $v[4] = shift(@vals) || 0;
157 $v[5] = shift(@vals) || 0;
158 }
159 $err = 0;
160 }
161 return 0 if $err or not @v;
162 if ($time) {
163 # zero any required shift entries which aren't yet defined
164 $v[0] = $v[1] = $v[2] = 0 if defined $$time[0] and not defined $v[0];
165 $v[3] = $v[4] = $v[5] = 0 if defined $$time[3] and not defined $v[3];
166 $v[6] = $v[7] = 0 if defined $$time[6] and not defined $v[6];
167 }
168 @$vals = @v; # return split time components
169 return 1;
170}
171
172#------------------------------------------------------------------------------
173# shift date/time by components
174# Inputs: 0) split date/time list ref, 1) split shift list ref,
175# 2) shift direction, 3) reference to output list of shifted components
176# 4) number of decimal points in seconds
177# 5) reference to return time difference due to rounding
178# Returns: error string or undef on success
179sub ShiftComponents($$$$$;$)
180{
181 my ($time, $shift, $dir, $toTime, $dec, $rndPt) = @_;
182 # min/max for Y, M, D, h, m, s
183 my @min = ( 0, 1, 1, 0, 0, 0);
184 my @max = (10000,12,28,24,60,60);
185 my $i;
186#
187# apply the shift
188#
189 my $c = 0;
190 for ($i=0; $i<@$time; ++$i) {
191 my $v = ($$time[$i] || 0) + $dir * ($$shift[$i] || 0) + $c;
192 # handle fractional values by propagating remainders downwards
193 if ($v != int($v) and $i < 5) {
194 my $iv = int($v);
195 $c = ($v - $iv) * $max[$i+1];
196 $v = $iv;
197 } else {
198 $c = 0;
199 }
200 $$toTime[$i] = $v;
201 }
202 # round off seconds to the required number of decimal points
203 my $sec = $$toTime[5];
204 if (defined $sec and $sec != int($sec)) {
205 my $mult = 10 ** $dec;
206 my $rndSec = int($sec * $mult + 0.5 * ($sec <=> 0)) / $mult;
207 $rndPt and $$rndPt = $sec - $rndSec;
208 $$toTime[5] = $rndSec;
209 }
210#
211# handle overflows, starting with least significant number first (seconds)
212#
213 $c = 0;
214 for ($i=5; $i>=0; $i--) {
215 defined $$time[$i] or $c = 0, next;
216 # apply shift and adjust for previous overflow
217 my $v = $$toTime[$i] + $c;
218 $c = 0; # set carry to zero
219 # adjust for over/underflow
220 my ($min, $max) = ($min[$i], $max[$i]);
221 if ($v < $min) {
222 if ($i == 2) { # 2 = day of month
223 do {
224 # add number of days in previous month
225 --$c;
226 my $mon = $$toTime[$i-1] + $c;
227 $v += DaysInMonth($mon, $$toTime[$i-2]);
228 } while ($v < 1);
229 } else {
230 my $fc = ($v - $min) / $max;
231 # carry ($c) must be largest integer equal to or less than $fc
232 $c = int($fc);
233 --$c if $c > $fc;
234 $v -= $c * $max;
235 }
236 } elsif ($v >= $max + $min) {
237 if ($i == 2) {
238 for (;;) {
239 # test against number of days in current month
240 my $mon = $$toTime[$i-1] + $c;
241 my $days = DaysInMonth($mon, $$toTime[$i-2]);
242 last if $v <= $days;
243 $v -= $days;
244 ++$c;
245 last if $v <= 28;
246 }
247 } else {
248 my $fc = ($v - $max - $min) / $max;
249 # carry ($c) must be smallest integer greater than $fc
250 $c = int($fc);
251 ++$c if $c <= $fc;
252 $v -= $c * $max;
253 }
254 }
255 $$toTime[$i] = $v; # save the new value
256 }
257 # handle overflows in timezone
258 if (defined $$toTime[6]) {
259 my $m = $$toTime[6] * 60 + $$toTime[7];
260 $m += 0.5 * ($m <=> 0); # avoid round-off errors
261 $$toTime[6] = int($m / 60);
262 $$toTime[7] = int($m - $$toTime[6] * 60);
263 }
264 return undef; # success
265}
266
267#------------------------------------------------------------------------------
268# Shift date/time string
269# Inputs: 0) date/time string, 1) shift string, 2) shift direction (+1 or -1)
270# 3) reference to ShiftOffset hash (with Date, DateTime, Time, Timezone keys)
271# Returns: error string or undef on success and date/time string is updated
272sub ShiftTime($$$;$)
273{
274 local $_;
275 my ($val, $shift, $dir, $shiftOffset) = @_;
276 my (@time, @shift, @toTime, $mode, $needShiftOffset, $dec);
277#
278# figure out what we are dealing with (time, date or date/time)
279#
280 SplitTime($val, \@time) or return "Invalid time string ($val)";
281 if (defined $time[0]) {
282 $mode = defined $time[3] ? 'DateTime' : 'Date';
283 } elsif (defined $time[3]) {
284 $mode = 'Time';
285 }
286 # get number of digits after the seconds decimal point
287 if (defined $time[5] and $time[5] =~ /\.(\d+)/) {
288 $dec = length($1);
289 } else {
290 $dec = 0;
291 }
292 if ($shiftOffset) {
293 $needShiftOffset = 1 unless defined $$shiftOffset{$mode};
294 $needShiftOffset = 1 if defined $time[6] and not defined $$shiftOffset{Timezone};
295 } else {
296 $needShiftOffset = 1;
297 }
298 if ($needShiftOffset) {
299#
300# apply date/time shift the hard way
301#
302 SplitTime($shift, \@shift, \@time) or return "Invalid shift string ($shift)";
303
304 # change 'Z' timezone to '+00:00' only if necessary
305 if (@shift > 6 and @time <= 6) {
306 $time[6] = $time[7] = 0 if $val =~ s/Z$/\+00:00/;
307 }
308 my $rndDiff;
309 my $err = ShiftComponents(\@time, \@shift, $dir, \@toTime, $dec, \$rndDiff);
310 $err and return $err;
311#
312# calculate and save the shift offsets for next time
313#
314 if ($shiftOffset) {
315 if (defined $time[0] or defined $time[3]) {
316 my @tm1 = (0, 0, 0, 1, 0, 2000);
317 my @tm2 = (0, 0, 0, 1, 0, 2000);
318 if (defined $time[0]) {
319 @tm1[3..5] = reverse @time[0..2];
320 @tm2[3..5] = reverse @toTime[0..2];
321 --$tm1[4]; # month should start from 0
322 --$tm2[4];
323 }
324 my $diff = 0;
325 if (defined $time[3]) {
326 @tm1[0..2] = reverse @time[3..5];
327 @tm2[0..2] = reverse @toTime[3..5];
328 # handle fractional seconds separately
329 $diff = $tm2[0] - int($tm2[0]) - ($tm1[0] - int($tm1[0]));
330 $diff += $rndDiff if defined $rndDiff; # un-do rounding
331 $tm1[0] = int($tm1[0]);
332 $tm2[0] = int($tm2[0]);
333 }
334 eval q{
335 require Time::Local;
336 $diff += Time::Local::timegm(@tm2) - Time::Local::timegm(@tm1);
337 };
338 # not a problem if we failed here since we'll just try again next time,
339 # so don't return error message
340 unless (@$) {
341 my $mode;
342 if (defined $time[0]) {
343 $mode = defined $time[3] ? 'DateTime' : 'Date';
344 } else {
345 $mode = 'Time';
346 }
347 $$shiftOffset{$mode} = $diff;
348 }
349 }
350 if (defined $time[6]) {
351 $$shiftOffset{Timezone} = ($toTime[6] - $time[6]) * 60 +
352 $toTime[7] - $time[7];
353 }
354 }
355
356 } else {
357#
358# apply shift from previously calculated offsets
359#
360 if ($$shiftOffset{Timezone} and @time <= 6) {
361 # change 'Z' timezone to '+00:00' only if necessary
362 $time[6] = $time[7] = 0 if $val =~ s/Z$/\+00:00/;
363 }
364 # apply the previous date/time shift if necessary
365 if ($mode) {
366 my @tm = (0, 0, 0, 1, 0, 2000);
367 if (defined $time[0]) {
368 @tm[3..5] = reverse @time[0..2];
369 --$tm[4]; # month should start from 0
370 }
371 @tm[0..2] = reverse @time[3..5] if defined $time[3];
372 # save fractional seconds
373 my $frac = $tm[0] - int($tm[0]);
374 $tm[0] = int($tm[0]);
375 my $tm;
376 eval q{
377 require Time::Local;
378 $tm = Time::Local::timegm(@tm) + $frac;
379 };
380 $@ and return CleanWarning($@);
381 $tm += $$shiftOffset{$mode}; # apply the shift
382 $tm < 0 and return 'Shift results in negative time';
383 # save fractional seconds in shifted time
384 $frac = $tm - int($tm);
385 if ($frac) {
386 $tm = int($tm);
387 # must account for any rounding that could occur
388 $frac + 0.5 * 10 ** (-$dec) >= 1 and ++$tm, $frac = 0;
389 }
390 @tm = gmtime($tm);
391 @toTime = reverse @tm[0..5];
392 $toTime[0] += 1900;
393 ++$toTime[1];
394 $toTime[5] += $frac; # add the fractional seconds back in
395 }
396 # apply the previous timezone shift if necessary
397 if (defined $time[6]) {
398 my $m = $time[6] * 60 + $time[7];
399 $m += $$shiftOffset{Timezone};
400 $m += 0.5 * ($m <=> 0); # avoid round-off errors
401 $toTime[6] = int($m / 60);
402 $toTime[7] = int($m - $toTime[6] * 60);
403 }
404 }
405#
406# insert shifted time components back into original string
407#
408 my ($i, $err);
409 for ($i=0; $i<@toTime; ++$i) {
410 next unless defined $time[$i] and defined $toTime[$i];
411 my ($v, $d, $s);
412 if ($i != 6) { # not timezone hours
413 last unless $val =~ /((?=\d|\.\d)\d*(\.\d*)?)/g;
414 next if $toTime[$i] == $time[$i];
415 $v = $1; # value
416 $d = $2; # decimal part of value
417 $s = ''; # no sign
418 } else {
419 last if $time[$i] == $toTime[$i] and $time[$i+1] == $toTime[$i+1];
420 last unless $val =~ /((?:\+|-)(?=\d|\.\d)\d*(\.\d*)?)/g;
421 $v = $1;
422 $d = $2;
423 if ($toTime[6] >= 0 and $toTime[7] >= 0) {
424 $s = '+';
425 } else {
426 $s = '-';
427 $toTime[6] = -$toTime[6];
428 $toTime[7] = -$toTime[7];
429 }
430 }
431 my $nv = $toTime[$i];
432 my $pos = pos $val;
433 my $len = length $v;
434 my $sig = $len - length $s;
435 my $dec = $d ? length($d) - 1 : 0;
436 my $newNum = sprintf($dec ? "$s%0$sig.${dec}f" : "$s%0${sig}d", $nv);
437 length($newNum) != $len and $err = 1;
438 substr($val, $pos - $len, $len) = $newNum;
439 pos($val) = $pos;
440 }
441 $err and return "Error packing shifted time ($val)";
442 $_[0] = $val; # return shifted value
443 return undef; # success!
444}
445
446
4471; # end
448
449__END__
450
451=head1 NAME
452
453Image::ExifTool::Shift.pl - ExifTool time shifting routines
454
455=head1 DESCRIPTION
456
457This module contains routines used by ExifTool to shift date and time
458values.
459
460=head1 DETAILS
461
462Time shifts are applied to standard EXIF-formatted date/time values (ie.
463C<2005:03:14 18:55:00>). Date-only and time-only values may also be
464shifted, and an optional timezone (ie. C<-05:00>) is also supported. Here
465are some general rules and examples to explain how shift strings are
466interpreted:
467
468Date-only values are shifted using the following formats:
469
470 'Y:M:D' - shift date by 'Y' years, 'M' months and 'D' days
471 'M:D' - shift months and days only
472 'D' - shift specified number of days
473
474Time-only values are shifted using the following formats:
475
476 'h:m:s' - shift time by 'h' hours, 'm' minutes and 's' seconds
477 'h:m' - shift hours and minutes only
478 'h' - shift specified number of hours
479
480Timezone shifts are specified in the following formats:
481
482 '+h:m' - shift timezone by 'h' hours and 'm' minutes
483 '-h:m' - negative shift of timezone hours and minutes
484 '+h' - shift timezone hours only
485 '-h' - negative shift of timezone hours only
486
487A valid shift value consists of one or two arguments, separated by a space.
488If only one is provided, it is assumed to be a time shift when applied to a
489time-only or a date/time value, or a date shift when applied to a date-only
490value. For example:
491
492 '7' - shift by 1 hour if applied to a time or date/time
493 value, or by one day if applied to a date value
494 '2:0' - shift 2 hours (time, date/time), or 2 months (date)
495 '5:0:0' - shift 5 hours (time, date/time), or 5 years (date)
496 '0:0:1' - shift 1 s (time, date/time), or 1 day (date)
497
498If two arguments are given, the date shift is first, followed by the time
499shift:
500
501 '3:0:0 0' - shift date by 3 years
502 '0 15:30' - shift time by 15 hours and 30 minutes
503 '1:0:0 0:0:0+5:0' - shift date by 1 year and timezone by 5 hours
504
505A date shift is simply ignored if applied to a time value or visa versa.
506
507Numbers specified in shift fields may contain a decimal point:
508
509 '1.5' - 1 hour 30 minutes (time, date/time), or 1 day (date)
510 '2.5 0' - 2 days 12 hours (date/time), 12 hours (time) or
511 2 days (date)
512
513And to save typing, a zero is assumed for any missing numbers:
514
515 '1::' - shift by 1 hour (time, date/time) or 1 year (date)
516 '26:: 0' - shift date by 26 years
517 '+:30 - shift timezone by 30 minutes
518
519Below are some specific examples applied to real date and/or time values
520('Dir' is the applied shift direction: '+' is positive, '-' is negative):
521
522 Original Value Shift Dir Shifted Value
523 --------------------- ------- --- ---------------------
524 '20:30:00' '5' + '01:30:00'
525 '2005:01:27' '5' + '2005:02:01'
526 '11:54:00' '2.5 0' - '23:54:00'
527 '2005:11:02' '2.5 0' - '2005:10:31'
528 '2005:11:02 11:54:00' '2.5 0' - '2005:10:30 23:54:00'
529 '2004:02:28 08:00:00' '1 1.3' + '2004:02:29 09:18:00'
530 '07:00:00' '-5' + '07:00:00'
531 '07:00:00+01:00' '-5' + '07:00:00-04:00'
532 '07:00:00Z' '+2:30' - '07:00:00-02:30'
533 '1970:01:01' '35::' + '2005:01:01'
534 '2005:01:01' '400' + '2006:02:05'
535 '10:00:00.00' '::1.33' + '09:59:58.67'
536
537=head1 NOTES
538
539The format of the original date/time value is not changed when the time
540shift is applied. This means that the length of the date/time string will
541not change, and only the numbers in the string will be modified. The only
542exception to this rule is that a 'Z' timezone is changed to '+00:00'
543notation if a timezone shift is applied. A timezone will not be added to
544the date/time string.
545
546=head1 TRICKY
547
548This module is perhaps more complicated than it needs to be because it is
549designed to be very flexible in the way time shifts are specified and
550applied...
551
552The ability to shift dates by Y years, M months, etc, is somewhat
553contradictory to the goal of maintaining a constant shift for all time
554values when applying a batch shift. This is because shifting by 1 month can
555be equivalent to anything from 28 to 31 days, and 1 year can be 365 or 366
556days, depending on the starting date.
557
558The inconsistency is handled by shifting the first tag found with the actual
559specified shift, then calculating the equivalent time difference in seconds
560for this shift and applying this difference to subsequent tags in a batch
561conversion. So if it works as designed, the behaviour should be both
562intuitive and mathematically correct, and the user shouldn't have to worry
563about details such as this (in keeping with Perl's "do the right thing"
564philosophy).
565
566=head1 BUGS
567
568This feature uses the standard time library functions, which typically are
569limited to dates in the range 1970 to 2038.
570
571=head1 AUTHOR
572
573Copyright 2003-2011, Phil Harvey (phil at owl.phy.queensu.ca)
574
575This library is free software; you can redistribute it and/or modify it
576under the same terms as Perl itself.
577
578=head1 SEE ALSO
579
580L<Image::ExifTool(3pm)|Image::ExifTool>
581
582=cut
Note: See TracBrowser for help on using the repository browser.