Shift.pl 22 KB

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