line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
# TimeZone::Solar |
2
|
|
|
|
|
|
|
# ABSTRACT: local solar timezone lookup and utilities including DateTime compatibility |
3
|
|
|
|
|
|
|
# part of Perl implementation of solar timezones library |
4
|
|
|
|
|
|
|
# |
5
|
|
|
|
|
|
|
# Copyright © 2020-2022 Ian Kluft. This program is free software; you can |
6
|
|
|
|
|
|
|
# redistribute it and/or modify it under the terms of the GNU General Public |
7
|
|
|
|
|
|
|
# License Version 3. See https://www.gnu.org/licenses/gpl-3.0-standalone.html |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
# pragmas to silence some warnings from Perl::Critic |
10
|
|
|
|
|
|
|
## no critic (Modules::RequireExplicitPackage) |
11
|
|
|
|
|
|
|
# This solves a catch-22 where parts of Perl::Critic want both package and use-strict to be first |
12
|
6
|
|
|
6
|
|
1141433
|
use strict; |
|
6
|
|
|
|
|
61
|
|
|
6
|
|
|
|
|
179
|
|
13
|
6
|
|
|
6
|
|
51
|
use warnings; |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
337
|
|
14
|
|
|
|
|
|
|
## use critic (Modules::RequireExplicitPackage) |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
package TimeZone::Solar; |
17
|
|
|
|
|
|
|
$TimeZone::Solar::VERSION = '0.2.0'; |
18
|
6
|
|
|
6
|
|
673
|
use utf8; |
|
6
|
|
|
|
|
33
|
|
|
6
|
|
|
|
|
37
|
|
19
|
6
|
|
|
6
|
|
3148
|
use autodie; |
|
6
|
|
|
|
|
90325
|
|
|
6
|
|
|
|
|
27
|
|
20
|
|
|
|
|
|
|
use overload |
21
|
6
|
|
|
|
|
39
|
'""' => "as_string", |
22
|
6
|
|
|
6
|
|
47418
|
'eq' => "eq_string"; |
|
6
|
|
|
|
|
4128
|
|
23
|
6
|
|
|
6
|
|
658
|
use Carp qw(croak); |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
314
|
|
24
|
6
|
|
|
6
|
|
37
|
use Readonly; |
|
6
|
|
|
|
|
12
|
|
|
6
|
|
|
|
|
338
|
|
25
|
6
|
|
|
6
|
|
2927
|
use DateTime::TimeZone qw(0.80); |
|
6
|
|
|
|
|
930980
|
|
|
6
|
|
|
|
|
250
|
|
26
|
6
|
|
|
6
|
|
51
|
use Try::Tiny; |
|
6
|
|
|
|
|
16
|
|
|
6
|
|
|
|
|
4854
|
|
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
# constants |
29
|
|
|
|
|
|
|
Readonly::Scalar my $debug_mode => ( exists $ENV{TZSOLAR_DEBUG} and $ENV{TZSOLAR_DEBUG} ) ? 1 : 0; |
30
|
|
|
|
|
|
|
Readonly::Scalar my $TZSOLAR_CLASS_PREFIX => "DateTime::TimeZone::Solar::"; |
31
|
|
|
|
|
|
|
Readonly::Scalar my $TZSOLAR_LON_ZONE_RE => qr((Lon0[0-9][0-9][EW]) | (Lon1[0-7][0-9][EW]) | (Lon180[EW]))x; |
32
|
|
|
|
|
|
|
Readonly::Scalar my $TZSOLAR_HOUR_ZONE_RE => qr((East|West)(0[0-9] | 1[0-2]))x; |
33
|
|
|
|
|
|
|
Readonly::Scalar my $TZSOLAR_ZONE_RE => qr( $TZSOLAR_LON_ZONE_RE | $TZSOLAR_HOUR_ZONE_RE )x; |
34
|
|
|
|
|
|
|
Readonly::Scalar my $PRECISION_DIGITS => 6; # max decimal digits of precision |
35
|
|
|
|
|
|
|
Readonly::Scalar my $PRECISION_FP => ( 10**-$PRECISION_DIGITS ) / 2.0; # 1/2 width of floating point equality |
36
|
|
|
|
|
|
|
Readonly::Scalar my $MAX_DEGREES => 360; # maximum degrees = 360 |
37
|
|
|
|
|
|
|
Readonly::Scalar my $MAX_LONGITUDE_INT => $MAX_DEGREES / 2; # min/max longitude in integer = 180 |
38
|
|
|
|
|
|
|
Readonly::Scalar my $MAX_LONGITUDE_FP => $MAX_DEGREES / 2.0; # min/max longitude in float = 180.0 |
39
|
|
|
|
|
|
|
Readonly::Scalar my $MAX_LATITUDE_FP => $MAX_DEGREES / 4.0; # min/max latitude in float = 90.0 |
40
|
|
|
|
|
|
|
Readonly::Scalar my $POLAR_UTC_AREA => 10; # latitude degrees around poles to use UTC |
41
|
|
|
|
|
|
|
Readonly::Scalar my $LIMIT_LATITUDE => $MAX_LATITUDE_FP - $POLAR_UTC_AREA; # max latitude for solar time zones |
42
|
|
|
|
|
|
|
Readonly::Scalar my $MINUTES_PER_DEGREE_LON => 4; # minutes per degree longitude |
43
|
|
|
|
|
|
|
Readonly::Hash my %constants => ( # allow tests to check constants |
44
|
|
|
|
|
|
|
PRECISION_DIGITS => $PRECISION_DIGITS, |
45
|
|
|
|
|
|
|
PRECISION_FP => $PRECISION_FP, |
46
|
|
|
|
|
|
|
MAX_DEGREES => $MAX_DEGREES, |
47
|
|
|
|
|
|
|
MAX_LONGITUDE_INT => $MAX_LONGITUDE_INT, |
48
|
|
|
|
|
|
|
MAX_LONGITUDE_FP => $MAX_LONGITUDE_FP, |
49
|
|
|
|
|
|
|
MAX_LATITUDE_FP => $MAX_LATITUDE_FP, |
50
|
|
|
|
|
|
|
POLAR_UTC_AREA => $POLAR_UTC_AREA, |
51
|
|
|
|
|
|
|
LIMIT_LATITUDE => $LIMIT_LATITUDE, |
52
|
|
|
|
|
|
|
MINUTES_PER_DEGREE_LON => $MINUTES_PER_DEGREE_LON, |
53
|
|
|
|
|
|
|
); |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
# create timezone subclass |
56
|
|
|
|
|
|
|
# this must be before the BEGIN block which uses it |
57
|
|
|
|
|
|
|
sub _tz_subclass |
58
|
|
|
|
|
|
|
{ |
59
|
2328
|
|
|
2328
|
|
4452
|
my ( $class, %opts ) = @_; |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
# for test coverage: if $opts{test_break_eval} is set, break the eval below |
62
|
|
|
|
|
|
|
# under normal circumstances, %opts parameters should be omitted |
63
|
|
|
|
|
|
|
my $result_cmd = ( |
64
|
|
|
|
|
|
|
( exists $opts{test_break_eval} and $opts{test_break_eval} ) |
65
|
2328
|
50
|
33
|
|
|
6003
|
? "croak 'break due to test_break_eval'" # for testing we can force the eval to break |
66
|
|
|
|
|
|
|
: "1" # normally the class definition returns 1 |
67
|
|
|
|
|
|
|
); |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
## no critic (BuiltinFunctions::ProhibitStringyEval) |
70
|
2328
|
|
|
|
|
3085
|
my $class_check = 0; |
71
|
|
|
|
|
|
|
try { |
72
|
2328
|
|
|
2328
|
|
306203
|
$class_check = |
73
|
|
|
|
|
|
|
eval "package $class {" . "\@" |
74
|
|
|
|
|
|
|
. $class |
75
|
|
|
|
|
|
|
. "::ISA = qw(" |
76
|
|
|
|
|
|
|
. __PACKAGE__ . ");" . "\$" |
77
|
|
|
|
|
|
|
. $class |
78
|
|
|
|
|
|
|
. "::VERSION = \$" |
79
|
|
|
|
|
|
|
. __PACKAGE__ |
80
|
|
|
|
|
|
|
. "::VERSION;" |
81
|
|
|
|
|
|
|
. "$result_cmd; " . "}"; |
82
|
2328
|
|
|
|
|
12049
|
}; |
83
|
2328
|
50
|
|
|
|
38220
|
if ( not $class_check ) { |
84
|
0
|
|
|
|
|
0
|
croak __PACKAGE__ . "::_tz_subclass: unable to create class $class"; |
85
|
|
|
|
|
|
|
} |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
# generate class file path for use in %INC so require() considers this class loaded |
88
|
2328
|
|
|
|
|
4095
|
my $classpath = $class; |
89
|
2328
|
|
|
|
|
10399
|
$classpath =~ s/::/\//gx; |
90
|
2328
|
|
|
|
|
4271
|
$classpath .= ".pm"; |
91
|
|
|
|
|
|
|
## no critic ( Variables::RequireLocalizedPunctuationVars) # this must be global to work |
92
|
2328
|
|
|
|
|
7746
|
$INC{$classpath} = 1; |
93
|
2328
|
|
|
|
|
4542
|
return; |
94
|
|
|
|
|
|
|
} |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
# create subclasses for DateTime::TimeZone::Solar::* time zones |
97
|
|
|
|
|
|
|
# Set subclass @ISA to point here as its parent. Then the subclass inherits methods from this class. |
98
|
|
|
|
|
|
|
# This modifies %DateTime::TimeZone::Catalog::LINKS the same way it allows DateTime::TimeZone::Alias to. |
99
|
|
|
|
|
|
|
BEGIN { |
100
|
|
|
|
|
|
|
# duplicate constant within BEGIN scope because it runs before constant assignments |
101
|
6
|
|
|
6
|
|
63
|
Readonly::Scalar my $TZSOLAR_CLASS_PREFIX => "DateTime::TimeZone::Solar::"; |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
# hour-based timezones from -12 to +12 |
104
|
6
|
|
|
|
|
256
|
foreach my $tz_dir (qw( East West )) { |
105
|
12
|
|
|
|
|
37
|
foreach my $tz_int ( 0 .. 12 ) { |
106
|
156
|
|
|
|
|
603
|
my $short_name = sprintf( "%s%02d", $tz_dir, $tz_int ); |
107
|
156
|
|
|
|
|
299
|
my $long_name = "Solar/" . $short_name; |
108
|
156
|
|
|
|
|
281
|
my $class_name = $TZSOLAR_CLASS_PREFIX . $short_name; |
109
|
156
|
|
|
|
|
364
|
_tz_subclass($class_name); |
110
|
156
|
|
|
|
|
637
|
$DateTime::TimeZone::Catalog::LINKS{$short_name} = $long_name; |
111
|
|
|
|
|
|
|
} |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
# longitude-based time zones from -180 to +180 |
115
|
6
|
|
|
|
|
14
|
foreach my $tz_dir (qw( E W )) { |
116
|
12
|
|
|
|
|
36
|
foreach my $tz_int ( 0 .. 180 ) { |
117
|
2172
|
|
|
|
|
7704
|
my $short_name = sprintf( "Lon%03d%s", $tz_int, $tz_dir ); |
118
|
2172
|
|
|
|
|
4404
|
my $long_name = "Solar/" . $short_name; |
119
|
2172
|
|
|
|
|
3493
|
my $class_name = $TZSOLAR_CLASS_PREFIX . $short_name; |
120
|
2172
|
|
|
|
|
4723
|
_tz_subclass($class_name); |
121
|
2172
|
|
|
|
|
15391
|
$DateTime::TimeZone::Catalog::LINKS{$short_name} = $long_name; |
122
|
|
|
|
|
|
|
} |
123
|
|
|
|
|
|
|
} |
124
|
|
|
|
|
|
|
} |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
# file globals |
127
|
|
|
|
|
|
|
my %_INSTANCES; |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
# enforce class access |
130
|
|
|
|
|
|
|
sub _class_guard |
131
|
|
|
|
|
|
|
{ |
132
|
83
|
|
|
83
|
|
10789
|
my $class = shift; |
133
|
83
|
100
|
|
|
|
186
|
my $classname = ref $class ? ref $class : $class; |
134
|
83
|
100
|
|
|
|
196
|
if ( not defined $classname ) { |
135
|
3
|
|
|
|
|
55
|
croak("incompatible class: invalid method call on undefined value"); |
136
|
|
|
|
|
|
|
} |
137
|
80
|
100
|
|
|
|
195
|
if ( not $class->isa(__PACKAGE__) ) { |
138
|
5
|
|
|
|
|
53
|
croak( "incompatible class: invalid method call for '$classname': not in " . __PACKAGE__ . " hierarchy" ); |
139
|
|
|
|
|
|
|
} |
140
|
75
|
|
|
|
|
145
|
return; |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
# Override isa() method from UNIVERSAL to trick DateTime::TimeZone to accept our timezones as its subclasses. |
144
|
|
|
|
|
|
|
# We don't inherit from DateTime::TimeZone as a base class because it's about Olson TZ db processing we don't need. |
145
|
|
|
|
|
|
|
# But DateTime uses DateTime::TimeZone to look up time zones, and this makes solar timezones fit in. |
146
|
|
|
|
|
|
|
## no critic ( Subroutines::ProhibitBuiltinHomonyms ) |
147
|
|
|
|
|
|
|
sub isa |
148
|
|
|
|
|
|
|
{ |
149
|
3398
|
|
|
3398
|
0
|
498531
|
my ( $class, $type ) = @_; |
150
|
3398
|
100
|
|
|
|
8196
|
if ( $type eq "DateTime::TimeZone" ) { |
151
|
56
|
|
|
|
|
117
|
return 1; |
152
|
|
|
|
|
|
|
} |
153
|
3342
|
|
|
|
|
16437
|
return $class->SUPER::isa($type); |
154
|
|
|
|
|
|
|
} |
155
|
|
|
|
|
|
|
## critic ( Subroutines::ProhibitBuiltinHomonyms ) |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
# access constants - for use by tests |
158
|
|
|
|
|
|
|
# if no name parameter is provided, return list of constant names |
159
|
|
|
|
|
|
|
# throws exception if requested contant name doesn't exist |
160
|
|
|
|
|
|
|
## no critic ( Subroutines::ProhibitUnusedPrivateSubroutines ) |
161
|
|
|
|
|
|
|
sub _get_const |
162
|
|
|
|
|
|
|
{ |
163
|
14
|
|
|
14
|
|
9470
|
my @args = @_; |
164
|
14
|
|
|
|
|
34
|
my ( $class, $name ) = @args; |
165
|
14
|
|
|
|
|
34
|
_class_guard($class); |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# if no name provided, return list of keys |
168
|
11
|
100
|
|
|
|
30
|
if ( scalar @args <= 1 ) { |
169
|
1
|
|
|
|
|
6
|
return ( sort keys %constants ); |
170
|
|
|
|
|
|
|
} |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
# require valid name parameter |
173
|
10
|
100
|
|
|
|
49
|
if ( not exists $constants{$name} ) { |
174
|
1
|
|
|
|
|
25
|
croak "non-existent constant requested: $name"; |
175
|
|
|
|
|
|
|
} |
176
|
9
|
|
|
|
|
89
|
return $constants{$name}; |
177
|
|
|
|
|
|
|
} |
178
|
|
|
|
|
|
|
## critic ( Subroutines::ProhibitUnusedPrivateSubroutines ) |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
# return TimeZone::Solar (or subclass) version number |
181
|
|
|
|
|
|
|
sub version |
182
|
|
|
|
|
|
|
{ |
183
|
4
|
|
|
4
|
1
|
3763
|
my $class = shift; |
184
|
4
|
|
|
|
|
13
|
_class_guard($class); |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
{ |
187
|
|
|
|
|
|
|
## no critic (TestingAndDebugging::ProhibitNoStrict) |
188
|
6
|
|
|
6
|
|
75
|
no strict 'refs'; |
|
6
|
|
|
|
|
19
|
|
|
6
|
|
|
|
|
40950
|
|
|
1
|
|
|
|
|
1
|
|
189
|
1
|
50
|
|
|
|
2
|
if ( defined ${ $class . "::VERSION" } ) { |
|
1
|
|
|
|
|
6
|
|
190
|
1
|
|
|
|
|
2
|
return ${ $class . "::VERSION" }; |
|
1
|
|
|
|
|
12
|
|
191
|
|
|
|
|
|
|
} |
192
|
|
|
|
|
|
|
} |
193
|
0
|
|
|
|
|
0
|
return "00-dev"; |
194
|
|
|
|
|
|
|
} |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# check latitude data and initialize special case for polar regions - internal method called by init() |
197
|
|
|
|
|
|
|
sub _tz_params_latitude |
198
|
|
|
|
|
|
|
{ |
199
|
115
|
|
|
115
|
|
199
|
my $param_ref = shift; |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
# safety check on latitude |
202
|
115
|
100
|
|
|
|
711
|
if ( not $param_ref->{latitude} =~ /^[-+]?\d+(\.\d+)?$/x ) { |
203
|
1
|
|
|
|
|
12
|
croak( __PACKAGE__ . "::_tz_params_latitude: latitude '" . $param_ref->{latitude} . "' is not numeric" ); |
204
|
|
|
|
|
|
|
} |
205
|
114
|
100
|
|
|
|
450
|
if ( abs( $param_ref->{latitude} ) > $MAX_LATITUDE_FP + $PRECISION_FP ) { |
206
|
2
|
|
|
|
|
20
|
croak __PACKAGE__ . "::_tz_params_latitude: latitude when provided must be in range -90..+90"; |
207
|
|
|
|
|
|
|
} |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
# special case: use East00/Lon000E (equal to UTC) within 10° latitude of poles |
210
|
112
|
100
|
|
|
|
284
|
if ( abs( $param_ref->{latitude} ) >= $LIMIT_LATITUDE - $PRECISION_FP ) { |
211
|
56
|
|
66
|
|
|
183
|
my $use_lon_tz = ( exists $param_ref->{use_lon_tz} and $param_ref->{use_lon_tz} ); |
212
|
56
|
100
|
|
|
|
142
|
$param_ref->{short_name} = $use_lon_tz ? "Lon000E" : "East00"; |
213
|
56
|
|
|
|
|
136
|
$param_ref->{name} = "Solar/" . $param_ref->{short_name}; |
214
|
56
|
|
|
|
|
94
|
$param_ref->{offset_min} = 0; |
215
|
56
|
|
|
|
|
101
|
$param_ref->{offset} = _offset_min2str(0); |
216
|
56
|
|
|
|
|
130
|
return $param_ref; |
217
|
|
|
|
|
|
|
} |
218
|
56
|
|
|
|
|
116
|
return; |
219
|
|
|
|
|
|
|
} |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
# formatting functions |
222
|
|
|
|
|
|
|
sub _tz_prefix |
223
|
|
|
|
|
|
|
{ |
224
|
1271
|
|
|
1271
|
|
2491
|
my ( $use_lon_tz, $sign ) = @_; |
225
|
1271
|
100
|
|
|
|
3872
|
return $use_lon_tz ? "Lon" : ( $sign > 0 ? "East" : "West" ); |
|
|
100
|
|
|
|
|
|
226
|
|
|
|
|
|
|
} |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
sub _tz_suffix |
229
|
|
|
|
|
|
|
{ |
230
|
1271
|
|
|
1271
|
|
2261
|
my ( $use_lon_tz, $sign ) = @_; |
231
|
1271
|
100
|
|
|
|
6502
|
return $use_lon_tz ? ( $sign > 0 ? "E" : "W" ) : ""; |
|
|
100
|
|
|
|
|
|
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# get timezone parameters (name and minutes offset) - called by new() |
235
|
|
|
|
|
|
|
sub _tz_params |
236
|
|
|
|
|
|
|
{ |
237
|
1334
|
|
|
1334
|
|
3866
|
my %params = @_; |
238
|
1334
|
100
|
|
|
|
3266
|
if ( not exists $params{longitude} ) { |
239
|
1
|
|
|
|
|
11
|
croak __PACKAGE__ . "::_tz_params: longitude parameter missing"; |
240
|
|
|
|
|
|
|
} |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# if latitude is provided, use UTC within 10° latitude of poles |
243
|
1333
|
100
|
|
|
|
2765
|
if ( exists $params{latitude} ) { |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
# check latitude data and special case for polar regions |
246
|
115
|
|
|
|
|
279
|
my $lat_params = _tz_params_latitude( \%params ); |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
# return if initialized, otherwise fall through to set time zone from longitude as usual |
249
|
112
|
100
|
|
|
|
299
|
return $lat_params |
250
|
|
|
|
|
|
|
if ref $lat_params eq "HASH"; |
251
|
|
|
|
|
|
|
} |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
# |
254
|
|
|
|
|
|
|
# set time zone from longitude |
255
|
|
|
|
|
|
|
# |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
# safety check on longitude |
258
|
1274
|
100
|
|
|
|
8016
|
if ( not $params{longitude} =~ /^[-+]?\d+(\.\d+)?$/x ) { |
259
|
1
|
|
|
|
|
12
|
croak( __PACKAGE__ . "::_tz_params: longitude '" . $params{longitude} . "' is not numeric" ); |
260
|
|
|
|
|
|
|
} |
261
|
1273
|
100
|
|
|
|
4319
|
if ( abs( $params{longitude} ) > $MAX_LONGITUDE_FP + $PRECISION_FP ) { |
262
|
2
|
|
|
|
|
23
|
croak __PACKAGE__ . "::_tz_params: longitude must be in the range -180 to +180"; |
263
|
|
|
|
|
|
|
} |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
# set flag for longitude time zones: 0 = hourly 1-hour/15-degree zones, 1 = longitude 4-minute/1-degree zones |
266
|
|
|
|
|
|
|
# defaults to hourly time zone ($use_lon_tz=0) |
267
|
1271
|
|
100
|
|
|
4925
|
my $use_lon_tz = ( exists $params{use_lon_tz} and $params{use_lon_tz} ); |
268
|
1271
|
100
|
|
|
|
2819
|
my $tz_degree_width = $use_lon_tz ? 1 : 15; # 1 for longitude-based tz, 15 for hour-based tz |
269
|
1271
|
100
|
|
|
|
2310
|
my $tz_digits = $use_lon_tz ? 3 : 2; |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
# handle special case of half-wide tz at positive side of solar date line (180° longitude) |
272
|
1271
|
100
|
100
|
|
|
6456
|
if ( $params{longitude} >= $MAX_LONGITUDE_INT - $tz_degree_width / 2.0 - $PRECISION_FP |
273
|
|
|
|
|
|
|
or $params{longitude} <= -$MAX_LONGITUDE_INT + $PRECISION_FP ) |
274
|
|
|
|
|
|
|
{ |
275
|
41
|
|
|
|
|
114
|
my $tz_name = sprintf "%s%0*d%s", |
276
|
|
|
|
|
|
|
_tz_prefix( $use_lon_tz, 1 ), |
277
|
|
|
|
|
|
|
$tz_digits, $MAX_LONGITUDE_INT / $tz_degree_width, |
278
|
|
|
|
|
|
|
_tz_suffix( $use_lon_tz, 1 ); |
279
|
41
|
|
|
|
|
136
|
$params{short_name} = $tz_name; |
280
|
41
|
|
|
|
|
122
|
$params{name} = "Solar/" . $tz_name; |
281
|
41
|
|
|
|
|
82
|
$params{offset_min} = 720; |
282
|
41
|
|
|
|
|
100
|
$params{offset} = _offset_min2str(720); |
283
|
41
|
|
|
|
|
122
|
return \%params; |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
# handle special case of half-wide tz at negativ< side of solar date line (180° longitude) |
287
|
1230
|
100
|
|
|
|
3259
|
if ( $params{longitude} <= -$MAX_LONGITUDE_INT + $tz_degree_width / 2.0 + $PRECISION_FP ) { |
288
|
14
|
|
|
|
|
47
|
my $tz_name = sprintf "%s%0*d%s", |
289
|
|
|
|
|
|
|
_tz_prefix( $use_lon_tz, -1 ), |
290
|
|
|
|
|
|
|
$tz_digits, $MAX_LONGITUDE_INT / $tz_degree_width, |
291
|
|
|
|
|
|
|
_tz_suffix( $use_lon_tz, -1 ); |
292
|
14
|
|
|
|
|
42
|
$params{short_name} = $tz_name; |
293
|
14
|
|
|
|
|
43
|
$params{name} = "Solar/" . $tz_name; |
294
|
14
|
|
|
|
|
27
|
$params{offset_min} = -720; |
295
|
14
|
|
|
|
|
32
|
$params{offset} = _offset_min2str(-720); |
296
|
14
|
|
|
|
|
49
|
return \%params; |
297
|
|
|
|
|
|
|
} |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
# handle other times zones |
300
|
1216
|
|
|
|
|
2682
|
my $tz_int = int( abs( $params{longitude} ) / $tz_degree_width + 0.5 + $PRECISION_FP ); |
301
|
1216
|
100
|
|
|
|
3007
|
my $sign = ( $params{longitude} > -$tz_degree_width / 2.0 + $PRECISION_FP ) ? 1 : -1; |
302
|
1216
|
|
|
|
|
2775
|
my $tz_name = sprintf "%s%0*d%s", |
303
|
|
|
|
|
|
|
_tz_prefix( $use_lon_tz, $sign ), |
304
|
|
|
|
|
|
|
$tz_digits, $tz_int, |
305
|
|
|
|
|
|
|
_tz_suffix( $use_lon_tz, $sign ); |
306
|
1216
|
|
|
|
|
2595
|
my $offset = $sign * $tz_int * ( $MINUTES_PER_DEGREE_LON * $tz_degree_width ); |
307
|
1216
|
|
|
|
|
2396
|
$params{short_name} = $tz_name; |
308
|
1216
|
|
|
|
|
3092
|
$params{name} = "Solar/" . $tz_name; |
309
|
1216
|
|
|
|
|
2103
|
$params{offset_min} = $offset; |
310
|
1216
|
|
|
|
|
2543
|
$params{offset} = _offset_min2str($offset); |
311
|
1216
|
|
|
|
|
3319
|
return \%params; |
312
|
|
|
|
|
|
|
} |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
# get timezone instance |
315
|
|
|
|
|
|
|
sub _tz_instance |
316
|
|
|
|
|
|
|
{ |
317
|
1331
|
|
|
1331
|
|
4873
|
my $hashref = shift; |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
# consistency checks |
320
|
1331
|
100
|
|
|
|
2886
|
if ( not defined $hashref ) { |
321
|
1
|
|
|
|
|
10
|
croak __PACKAGE__ . "::_tz_instance: object not found in parameters"; |
322
|
|
|
|
|
|
|
} |
323
|
1330
|
100
|
|
|
|
3425
|
if ( ref $hashref ne "HASH" ) { |
324
|
1
|
|
|
|
|
12
|
croak __PACKAGE__ . "::_tz_instance: received non-hash " . ( ref $hashref ) . " for object"; |
325
|
|
|
|
|
|
|
} |
326
|
1329
|
100
|
|
|
|
2845
|
if ( not exists $hashref->{short_name} ) { |
327
|
1
|
|
|
|
|
9
|
croak __PACKAGE__ . "::_tz_instance: short_name attribute missing"; |
328
|
|
|
|
|
|
|
} |
329
|
1328
|
100
|
|
|
|
8529
|
if ( $hashref->{short_name} !~ $TZSOLAR_ZONE_RE ) { |
330
|
|
|
|
|
|
|
croak __PACKAGE__ |
331
|
|
|
|
|
|
|
. "::_tz_instance: short_name attrbute " |
332
|
|
|
|
|
|
|
. $hashref->{short_name} |
333
|
1
|
|
|
|
|
15
|
. " is not a valid Solar timezone"; |
334
|
|
|
|
|
|
|
} |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# look up class instance, return it if found |
337
|
1327
|
|
|
|
|
3348
|
my $class = $TZSOLAR_CLASS_PREFIX . $hashref->{short_name}; |
338
|
1327
|
100
|
|
|
|
3768
|
if ( exists $_INSTANCES{$class} ) { |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
# forward lat/lon parameters to the existing instance, mainly so tests can see where it came from |
341
|
517
|
|
|
|
|
1021
|
foreach my $key (qw(longitude latitude)) { |
342
|
1034
|
100
|
|
|
|
2035
|
if ( exists $hashref->{$key} ) { |
343
|
628
|
|
|
|
|
2037
|
$_INSTANCES{$class}->{$key} = $hashref->{$key}; |
344
|
|
|
|
|
|
|
} else { |
345
|
406
|
|
|
|
|
847
|
delete $_INSTANCES{$class}->{$key}; |
346
|
|
|
|
|
|
|
} |
347
|
|
|
|
|
|
|
} |
348
|
517
|
|
|
|
|
1180
|
return $_INSTANCES{$class}; |
349
|
|
|
|
|
|
|
} |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
# make sure the new singleton object's class is a subclass of TimeZone::Solar |
352
|
|
|
|
|
|
|
# this should have already been done by the BEGIN block for all solar timezone subclasses |
353
|
810
|
50
|
|
|
|
8170
|
if ( not $class->isa(__PACKAGE__) ) { |
354
|
0
|
|
|
|
|
0
|
_tz_subclass($class); |
355
|
|
|
|
|
|
|
} |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
# bless the new object into the timezone subclass and save the singleton instance |
358
|
810
|
|
|
|
|
1783
|
my $obj = bless $hashref, $class; |
359
|
810
|
|
|
|
|
1720
|
$_INSTANCES{$class} = $obj; |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
# return the new object |
362
|
810
|
|
|
|
|
1442
|
return $obj; |
363
|
|
|
|
|
|
|
} |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
# instantiate a new TimeZone::Solar object |
366
|
|
|
|
|
|
|
sub new |
367
|
|
|
|
|
|
|
{ |
368
|
1335
|
|
|
1335
|
1
|
729152
|
my ( $in_class, %args ) = @_; |
369
|
1335
|
|
33
|
|
|
6951
|
my $class = ref($in_class) || $in_class; |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
# safety check |
372
|
1335
|
100
|
|
|
|
3270
|
if ( not $class->isa(__PACKAGE__) ) { |
373
|
1
|
|
|
|
|
37
|
croak __PACKAGE__ . "->new() prohibited for unrelated class $class"; |
374
|
|
|
|
|
|
|
} |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
# if we got here via DataTime::TimeZone::Solar::*->new(), override longitude/use_lon_tz parameters from class name |
377
|
1334
|
100
|
|
|
|
14469
|
if ( $in_class =~ qr( $TZSOLAR_CLASS_PREFIX ( $TZSOLAR_ZONE_RE ))x ) { |
378
|
451
|
|
|
|
|
1318
|
my $in_tz = $1; |
379
|
451
|
100
|
|
|
|
1933
|
if ( substr( $in_tz, 0, 4 ) eq "East" ) { |
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
380
|
31
|
|
|
|
|
93
|
my $tz_int = int substr( $in_tz, 4, 2 ); |
381
|
31
|
|
|
|
|
70
|
$args{longitude} = $tz_int * 15; |
382
|
31
|
|
|
|
|
63
|
$args{use_lon_tz} = 0; |
383
|
|
|
|
|
|
|
} elsif ( substr( $in_tz, 0, 4 ) eq "West" ) { |
384
|
29
|
|
|
|
|
82
|
my $tz_int = int substr( $in_tz, 4, 2 ); |
385
|
29
|
|
|
|
|
65
|
$args{longitude} = -$tz_int * 15; |
386
|
29
|
|
|
|
|
71
|
$args{use_lon_tz} = 0; |
387
|
|
|
|
|
|
|
} elsif ( substr( $in_tz, 0, 3 ) eq "Lon" ) { |
388
|
391
|
|
|
|
|
976
|
my $tz_int = int substr( $in_tz, 3, 3 ); |
389
|
391
|
100
|
|
|
|
832
|
my $sign = ( substr( $in_tz, 6, 1 ) eq "E" ? 1 : -1 ); |
390
|
391
|
|
|
|
|
858
|
$args{longitude} = $sign * $tz_int; |
391
|
391
|
|
|
|
|
798
|
$args{use_lon_tz} = 1; |
392
|
|
|
|
|
|
|
} else { |
393
|
0
|
|
|
|
|
0
|
croak __PACKAGE__ . "->new() received unrecognized class name $in_class"; |
394
|
|
|
|
|
|
|
} |
395
|
451
|
|
|
|
|
757
|
delete $args{latitude}; |
396
|
|
|
|
|
|
|
} |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
# use %args to look up a timezone singleton instance |
399
|
|
|
|
|
|
|
# make a new one if it doesn't yet exist |
400
|
1334
|
|
|
|
|
5095
|
my $tz_params = _tz_params(%args); |
401
|
1327
|
|
|
|
|
2844
|
my $self = _tz_instance($tz_params); |
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
# use init() method, with support for derived classes that may override it |
404
|
1327
|
50
|
|
|
|
8617
|
if ( my $init_func = $self->can("init") ) { |
405
|
0
|
|
|
|
|
0
|
$init_func->($self); |
406
|
|
|
|
|
|
|
} |
407
|
1327
|
|
|
|
|
6348
|
return $self; |
408
|
|
|
|
|
|
|
} |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
# |
411
|
|
|
|
|
|
|
# accessor methods |
412
|
|
|
|
|
|
|
# |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
# longitude: read-only accessor |
415
|
|
|
|
|
|
|
sub longitude |
416
|
|
|
|
|
|
|
{ |
417
|
125
|
|
|
125
|
1
|
4935
|
my $self = shift; |
418
|
125
|
|
|
|
|
825
|
return $self->{longitude}; |
419
|
|
|
|
|
|
|
} |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
# latitude read-only accessor |
422
|
|
|
|
|
|
|
sub latitude |
423
|
|
|
|
|
|
|
{ |
424
|
112
|
|
|
112
|
1
|
191
|
my $self = shift; |
425
|
112
|
50
|
|
|
|
238
|
return if not exists $self->{latitude}; |
426
|
112
|
|
|
|
|
236
|
return $self->{latitude}; |
427
|
|
|
|
|
|
|
} |
428
|
|
|
|
|
|
|
|
429
|
|
|
|
|
|
|
# name: read accessor |
430
|
|
|
|
|
|
|
sub name |
431
|
|
|
|
|
|
|
{ |
432
|
822
|
|
|
822
|
1
|
1492
|
my $self = shift; |
433
|
822
|
|
|
|
|
7588
|
return $self->{name}; |
434
|
|
|
|
|
|
|
} |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
# short_name: read accessor |
437
|
|
|
|
|
|
|
sub short_name |
438
|
|
|
|
|
|
|
{ |
439
|
987
|
|
|
987
|
1
|
247051
|
my $self = shift; |
440
|
987
|
|
|
|
|
20441
|
return $self->{short_name}; |
441
|
|
|
|
|
|
|
} |
442
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
# long_name: read accessor |
444
|
750
|
|
|
750
|
1
|
1797
|
sub long_name { my $self = shift; return $self->name(); } |
|
750
|
|
|
|
|
2470
|
|
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
# offset read accessor |
447
|
|
|
|
|
|
|
sub offset |
448
|
|
|
|
|
|
|
{ |
449
|
890
|
|
|
890
|
1
|
1798
|
my $self = shift; |
450
|
890
|
|
|
|
|
5487
|
return $self->{offset}; |
451
|
|
|
|
|
|
|
} |
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
# offset_min read accessor |
454
|
|
|
|
|
|
|
sub offset_min |
455
|
|
|
|
|
|
|
{ |
456
|
834
|
|
|
834
|
1
|
1683
|
my $self = shift; |
457
|
834
|
|
|
|
|
5021
|
return $self->{offset_min}; |
458
|
|
|
|
|
|
|
} |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
# |
461
|
|
|
|
|
|
|
# conversion functions |
462
|
|
|
|
|
|
|
# |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
# convert offset minutes to string |
465
|
|
|
|
|
|
|
sub _offset_min2str |
466
|
|
|
|
|
|
|
{ |
467
|
1327
|
|
|
1327
|
|
2513
|
my $offset_min = shift; |
468
|
1327
|
100
|
|
|
|
2566
|
my $sign = $offset_min >= 0 ? "+" : "-"; |
469
|
1327
|
|
|
|
|
2418
|
my $hours = int( abs($offset_min) / 60 ); |
470
|
1327
|
|
|
|
|
2250
|
my $minutes = abs($offset_min) % 60; |
471
|
1327
|
|
|
|
|
4835
|
return sprintf "%s%02d%s%02d", $sign, $hours, ":", $minutes; |
472
|
|
|
|
|
|
|
} |
473
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
# offset minutes as string |
475
|
|
|
|
|
|
|
sub offset_str |
476
|
|
|
|
|
|
|
{ |
477
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
478
|
0
|
|
|
|
|
0
|
return $self->{offset}; |
479
|
|
|
|
|
|
|
} |
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
# convert offset minutes to seconds |
482
|
|
|
|
|
|
|
sub offset_sec |
483
|
|
|
|
|
|
|
{ |
484
|
56
|
|
|
56
|
1
|
82
|
my $self = shift; |
485
|
56
|
|
|
|
|
146
|
return $self->{offset_min} * 60; |
486
|
|
|
|
|
|
|
} |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
# |
489
|
|
|
|
|
|
|
# DateTime::TimeZone interface compatibility methods |
490
|
|
|
|
|
|
|
# By definition, there is never a Daylight Savings change in the Solar time zones. |
491
|
|
|
|
|
|
|
# |
492
|
0
|
|
|
0
|
1
|
0
|
sub spans { return []; } |
493
|
0
|
|
|
0
|
1
|
0
|
sub has_dst_changes { return 0; } |
494
|
87
|
|
|
87
|
1
|
1465
|
sub is_floating { return 0; } |
495
|
76
|
|
|
76
|
1
|
8702
|
sub is_olson { return 0; } |
496
|
13
|
|
|
13
|
1
|
60
|
sub category { return "Solar"; } |
497
|
48
|
100
|
|
48
|
1
|
17414
|
sub is_utc { my $self = shift; return $self->{offset_min} == 0 ? 1 : 0; } |
|
48
|
|
|
|
|
144
|
|
498
|
26
|
|
|
26
|
1
|
113
|
sub is_dst_for_datetime { return 0; } |
499
|
24
|
|
|
24
|
1
|
159
|
sub offset_for_datetime { my $self = shift; return $self->offset_sec(); } |
|
24
|
|
|
|
|
50
|
|
500
|
32
|
|
|
32
|
1
|
240
|
sub offset_for_local_datetime { my $self = shift; return $self->offset_sec(); } |
|
32
|
|
|
|
|
63
|
|
501
|
13
|
|
|
13
|
1
|
30
|
sub short_name_for_datetime { my $self = shift; return $self->short_name(); } |
|
13
|
|
|
|
|
55
|
|
502
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
# instance method to respond to DateTime::TimeZone as it expects its timezone subclasses to |
504
|
|
|
|
|
|
|
sub instance |
505
|
|
|
|
|
|
|
{ |
506
|
63
|
|
|
63
|
1
|
27557
|
my ( $class, %args ) = @_; |
507
|
63
|
|
|
|
|
185
|
_class_guard($class); |
508
|
63
|
|
|
|
|
122
|
delete $args{is_olson}; # never accept the is_olson attribute since it isn't true for solar timezones |
509
|
63
|
|
|
|
|
214
|
return $class->new(%args); |
510
|
|
|
|
|
|
|
} |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
# convert to string for printing |
513
|
|
|
|
|
|
|
# used to overload "" (to string) operator |
514
|
|
|
|
|
|
|
sub as_string |
515
|
|
|
|
|
|
|
{ |
516
|
56
|
|
|
56
|
0
|
1268
|
my $self = shift; |
517
|
56
|
|
|
|
|
117
|
return $self->name() . " " . $self->offset(); |
518
|
|
|
|
|
|
|
} |
519
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
# equality comparison |
521
|
|
|
|
|
|
|
# used to overload eq (string equality) operator |
522
|
|
|
|
|
|
|
sub eq_string |
523
|
|
|
|
|
|
|
{ |
524
|
8
|
|
|
8
|
0
|
18725
|
my ( $self, $arg ) = @_; |
525
|
8
|
50
|
33
|
|
|
95
|
if ( ref $arg and $arg->isa(__PACKAGE__) ) { |
526
|
0
|
|
|
|
|
0
|
return $self->name eq $arg->name; |
527
|
|
|
|
|
|
|
} |
528
|
8
|
|
|
|
|
42
|
return $self->name eq $arg; |
529
|
|
|
|
|
|
|
} |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
1; |
532
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
__END__ |
534
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
=pod |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
=encoding UTF-8 |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
=head1 NAME |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
TimeZone::Solar - local solar timezone lookup and utilities including DateTime compatibility |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
=head1 VERSION |
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
version 0.2.0 |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=head1 SYNOPSIS |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
Using TimeZone::Solar alone, with longitude only: |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
use TimeZone::Solar; |
552
|
|
|
|
|
|
|
use feature qw(say); |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
# example without latitude - assumes between 80N and 80S latitude |
555
|
|
|
|
|
|
|
my $solar_tz = TimeZone::Solar->new( longitude => -121.929 ); |
556
|
|
|
|
|
|
|
say join " ", ( $solar_tz->name, $solar_tz->short_name, $solar_tz->offset, |
557
|
|
|
|
|
|
|
$solar_tz->offset_min ); |
558
|
|
|
|
|
|
|
|
559
|
|
|
|
|
|
|
This outputs "Solar/West08 West08 -08:00 -480" using an hour-based time zone. |
560
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
Using TimeZone::Solar alone, with longitude and latitude: |
562
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
use TimeZone::Solar; |
564
|
|
|
|
|
|
|
use feature qw(say); |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
# example with latitude (location: SJC airport, San Jose, California) |
567
|
|
|
|
|
|
|
my $solar_tz_lat = TimeZone::Solar->new( latitude => 37.363, |
568
|
|
|
|
|
|
|
longitude => -121.929, use_lon_tz => 1 ); |
569
|
|
|
|
|
|
|
say $solar_tz_lat; |
570
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
This outputs "Solar/Lon122W -08:08" using a longitude-based time zone. |
572
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
Using TimeZone::Solar with DateTime: |
574
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
use DateTime; |
576
|
|
|
|
|
|
|
use TimeZone::Solar; |
577
|
|
|
|
|
|
|
use feature qw(say); |
578
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
# noon local solar time at 122W longitude, i.e. San Jose CA or Seattle WA |
580
|
|
|
|
|
|
|
my $dt = DateTime->new( year => 2022, month => 6, hour => 1, |
581
|
|
|
|
|
|
|
time_zone => "Solar/West08" ); |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
# convert to US Pacific Time (works for Standard or Daylight conversion) |
584
|
|
|
|
|
|
|
$dt->set_time_zone( "US/Pacific" ); |
585
|
|
|
|
|
|
|
say $dt; |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
This code prints "2022-06-01T13:00:00", which means noon was converted to |
588
|
|
|
|
|
|
|
1PM, because Solar/West08 is equivalent to US Pacific Standard Time, |
589
|
|
|
|
|
|
|
centered on 120W longitude. And Standard Time is 1 hour off from Daylight |
590
|
|
|
|
|
|
|
Time, which changed noon Solar Time to 1PM Daylight Time. |
591
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
=head1 DESCRIPTION |
593
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
I<TimeZone::Solar> provides lookup and conversion utilities for Solar time zones, which are based on |
595
|
|
|
|
|
|
|
the longitude of any location on Earth. See the next subsection below for more information. |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
Through compatibility with L<DateTime::TimeZone>, I<TimeZone::Solar> allows the L<DateTime> module to |
598
|
|
|
|
|
|
|
convert either direction between standard (Olson Database) timezones and Solar time zones. |
599
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
=head2 Overview of Solar time zones |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
Solar time zones are based on the longitude of a location. Each time zone is defined around having |
603
|
|
|
|
|
|
|
local solar noon, on average, the same as noon on the clock. |
604
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
Solar time zones are always in Standard Time. There are no Daylight Time changes, by definition. The main point |
606
|
|
|
|
|
|
|
is to have a way to opt out of Daylight Saving Time by using solar time. |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
The Solar time zones build upon existing standards. |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
=over |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
=item * |
613
|
|
|
|
|
|
|
Lines of longitude are a well-established standard. |
614
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
=item * |
616
|
|
|
|
|
|
|
Ships at sea use "nautical time" based on time zones 15 degrees of longitude wide. |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
=item * |
619
|
|
|
|
|
|
|
Time zones (without daylight saving offsets) are based on average solar noon at the Prime Meridian. Standard Time in |
620
|
|
|
|
|
|
|
each time zone lines up with average solar noon on the meridian at the center of each time zone, at 15-degree of |
621
|
|
|
|
|
|
|
longitude increments. |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
=back |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
15 degrees of longitude appears more than once above. That isn't a coincidence. It's derived from 360 degrees |
626
|
|
|
|
|
|
|
of rotation in a day, divided by 24 hours in a day. The result is 15 degrees of longitude representing 1 hour |
627
|
|
|
|
|
|
|
in Earth's rotation. That makes each time zone one hour wide. So Solar time zones use that too. |
628
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
The Solar Time Zones proposal is intended as a potential de-facto standard which people can use in their |
630
|
|
|
|
|
|
|
local areas, providing for routine computational time conversion to and from local standard or daylight time. |
631
|
|
|
|
|
|
|
In order for the proposal to become a de-facto standard, made in force by the number of people using it, |
632
|
|
|
|
|
|
|
it starts with technical early adopters choosing to use it. At some point it would actually become an |
633
|
|
|
|
|
|
|
official alternative via publication of an Internet RFC and adding the new time zones into the |
634
|
|
|
|
|
|
|
Internet Assigned Numbers Authority (IANA) Time Zone Database files. The Time Zone Database feeds |
635
|
|
|
|
|
|
|
the time zone conversions used by computers including servers, desktops, phones and embedded devices. |
636
|
|
|
|
|
|
|
|
637
|
|
|
|
|
|
|
There are normal variations of a matter of minutes between local solar noon and clock noon, depending on |
638
|
|
|
|
|
|
|
the latitude and time of year. That variation is always the same number of minutes as local solar noon |
639
|
|
|
|
|
|
|
differs from noon UTC at the same latitude on the Prime Meridian (0° longitude), due to seasonal effects |
640
|
|
|
|
|
|
|
of the tilt in Earth's axis relative to our orbit around the Sun. |
641
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
The Solaer time zones also have another set of overlay time zones the width of 1 degree of longitude, which puts |
643
|
|
|
|
|
|
|
them in 4-minute intervals of time. These are a hyper-local niche for potential use by outdoor events or activities |
644
|
|
|
|
|
|
|
which must be scheduled around daylight. They can also be used by anyone who wants the middle of the scheduling day |
645
|
|
|
|
|
|
|
to coincide closely with local solar noon. |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
=head2 Definition of Solar time zones |
648
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
The Solar time zones definition includes the following rules. |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
=over |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
=item * |
654
|
|
|
|
|
|
|
There are 24 hour-based Solar Time Zones, named West12, West11, West10, West09 through East12. East00 is equivalent to UTC. West00 is an alias for East00. |
655
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
=over |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
=item * |
659
|
|
|
|
|
|
|
Hour-based time zones are spaced in one-hour time increments, or 15 degrees of longitude. |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
=item * |
662
|
|
|
|
|
|
|
Each hour-based time zone is centered on a meridian at a multiple of 15 degrees. In positive and negative integers, these are 0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165 and 180. |
663
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
=item * |
665
|
|
|
|
|
|
|
Each hour-based time zone spans the area ±7.5 degrees of longitude either side of its meridian. |
666
|
|
|
|
|
|
|
|
667
|
|
|
|
|
|
|
=back |
668
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
=item * |
670
|
|
|
|
|
|
|
There are 360 longitude-based Solar Time Zones, named Lon180W for 180 degrees West through Lon180E for 180 degrees East. Lon000E is equivalent to UTC. Lon000W is an alias for Lon000E. |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
=over |
673
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
=item * |
675
|
|
|
|
|
|
|
Longitude-based time zones are spaced in 4-minute time increments, or 1 degree of longitude. |
676
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
=item * |
678
|
|
|
|
|
|
|
Each longitude-based time zone is centered on the meridian of an integer degree of longitude. |
679
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
=item * |
681
|
|
|
|
|
|
|
Each longitude-based time zone spans the area ±0.5 degrees of longitude either side of its meridian. |
682
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
=back |
684
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
=item * |
686
|
|
|
|
|
|
|
In both hourly and longitude-based time zones, there is a limit to their usefulness at the poles. Beyond 80 degrees north or south, the definition uses UTC (East00 or Lon000E). This boundary is the only reason to include latitude in the computation of the time zone. |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
=item * |
689
|
|
|
|
|
|
|
When converting coordinates to a time zone, each time zone includes its boundary meridian at the lower end of its absolute value, which is in the direction toward 0 (UTC). The exception is at exactly ±180.0 degrees, which would be excluded from both sides by this rule. That case is arbitrarily set as +180 just to pick one. |
690
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
=item * |
692
|
|
|
|
|
|
|
The category "Solar" is used for the longer names for these time zones. The names listed above are the short names. The full long name of each time zone is prefixed with "Solar/" such as "Solar/East00" or "Solar/Lon000E". |
693
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
=back |
695
|
|
|
|
|
|
|
|
696
|
|
|
|
|
|
|
=head1 FUNCTIONS AND METHODS |
697
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
=head2 Class methods |
699
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
=over |
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
=item $obj = TimeZone::Solar->new( longitude => $float, use_lon_tz => $bool, [latitude => $float] ) |
703
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
Create a new instance of the time zone for the given longitude as a floating point number. The "use_lon_tz" parameter |
705
|
|
|
|
|
|
|
is a boolean flag which if true selects longitude-based time zones, at a width of 1 degree of longitude. If false or |
706
|
|
|
|
|
|
|
omitted, it selects hour-based time zones, at a width of 15 degrees of longitude. |
707
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
If a latitude parameter is provided, it only makes a difference if the latitude is within 10° of the poles, |
709
|
|
|
|
|
|
|
at or beyond 80° North or South latitude. In the polar regions, it uses the equivalent of UTC, which is Solar/East00 |
710
|
|
|
|
|
|
|
for hour-based time zones or Solar/Lon000E for longitude-based time zones. |
711
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
I<TimeZone::Solar> uses a singleton pattern. So if a given solar time zone's class within the |
713
|
|
|
|
|
|
|
I<DateTime::TimeZone::Solar::*> hierarchy already has an instance, that one will be returned. |
714
|
|
|
|
|
|
|
A new instance is only returned the first time. |
715
|
|
|
|
|
|
|
|
716
|
|
|
|
|
|
|
=item $obj = DateTime::TimeZone::Solar::I<timezone>->new() |
717
|
|
|
|
|
|
|
|
718
|
|
|
|
|
|
|
This is the same class method as TimeZone::Solar->new() except that if called with a class in the |
719
|
|
|
|
|
|
|
I<DateTime::TimeZone::Solar::*> hierarchy, it obtains the time zone parameters from the class name. |
720
|
|
|
|
|
|
|
If an instance exists for that solar time zone class, then that instance is returned. |
721
|
|
|
|
|
|
|
If not, a new one is instantiated and returned. |
722
|
|
|
|
|
|
|
|
723
|
|
|
|
|
|
|
=item $obj = DateTime::TimeZone::Solar::I<timezone>->instance() |
724
|
|
|
|
|
|
|
|
725
|
|
|
|
|
|
|
For compatibility with I<DateTime::TimeZone>, the instance() method returns the class' instance if it exists. |
726
|
|
|
|
|
|
|
Otherwise it is created using the class name to fill in its parameters via the new() method. |
727
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
=item TimeZone::Solar->version() |
729
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
Return the version number of TimeZone::Solar, or for any subclass which inherits the method. |
731
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
When running code within a source-code development workspace, it returns "00-dev" to avoid warnings |
733
|
|
|
|
|
|
|
about undefined values. |
734
|
|
|
|
|
|
|
Release version numbers are assigned and added by the build system upon release, |
735
|
|
|
|
|
|
|
and are not available when running directly from a source code repository. |
736
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
=back |
738
|
|
|
|
|
|
|
|
739
|
|
|
|
|
|
|
=head2 instance methods |
740
|
|
|
|
|
|
|
|
741
|
|
|
|
|
|
|
=over |
742
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
=item $obj->longitude() |
744
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
returns the longitude which was used to instantiate the time zone object. |
746
|
|
|
|
|
|
|
This is mainly intended for testing. Once instantiated the time zone object serves all areas in its boundary. |
747
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
=item $obj->latitude() |
749
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
returns the latitude which was used to instantiate the time zone object, or undef if none was provided. |
751
|
|
|
|
|
|
|
This is mainly intended for testing. Once instantiated the time zone object serves all areas in its boundary. |
752
|
|
|
|
|
|
|
|
753
|
|
|
|
|
|
|
=item $obj->name() |
754
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
returns a string with the long name, including the "Solar/" prefix, of the time zone. |
756
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
=item $obj->long_name() |
758
|
|
|
|
|
|
|
|
759
|
|
|
|
|
|
|
returns a string with the long name, including the "Solar/" prefix, of the time zone. |
760
|
|
|
|
|
|
|
This is equivalent to $obj->name(). |
761
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
=item $obj->short_name() |
763
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
returns a string with the short name, excluding the "Solar/" prefix, of the time zone. |
765
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
=item $obj->offset() |
767
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
returns a string with the time zone's offset from UTC in hour-minute format like +01:01 or -01:01 . |
769
|
|
|
|
|
|
|
If seconds matter, it will include them in the format +01:01:01 or -01:01:01 . |
770
|
|
|
|
|
|
|
|
771
|
|
|
|
|
|
|
=item $obj->offset_str() |
772
|
|
|
|
|
|
|
|
773
|
|
|
|
|
|
|
returns a string with the time zone's offset from UTC in hour-minute format, equivalent to $obj->offset(). |
774
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
=item $obj->offset_min() |
776
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
returns an integer with the number of minutes of the time zone's offest from UTC. |
778
|
|
|
|
|
|
|
|
779
|
|
|
|
|
|
|
=item $obj->offset_sec() |
780
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
returns an integer with the number of seconds of the time zone's offest from UTC. |
782
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
=back |
784
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
=head2 DateTime::TimeZone compatibility methods |
786
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
=over |
788
|
|
|
|
|
|
|
|
789
|
|
|
|
|
|
|
=item spans() |
790
|
|
|
|
|
|
|
|
791
|
|
|
|
|
|
|
always returns an empty list because there are never any Daylight Time transitions in solar time zones. |
792
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
=item has_dst_changes() |
794
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
always returns 0 (false) because there are never any Daylight Time transitions in solar time zones. |
796
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
=item is_floating() |
798
|
|
|
|
|
|
|
|
799
|
|
|
|
|
|
|
always returns 0 (false) because the solar time zones are not floating time zones. |
800
|
|
|
|
|
|
|
|
801
|
|
|
|
|
|
|
=item is_olson() |
802
|
|
|
|
|
|
|
|
803
|
|
|
|
|
|
|
always returns 0 (false) because the solar time zones are not in the Olson time zone database. |
804
|
|
|
|
|
|
|
(Maybe some day.) |
805
|
|
|
|
|
|
|
|
806
|
|
|
|
|
|
|
=item category() |
807
|
|
|
|
|
|
|
|
808
|
|
|
|
|
|
|
always returns "Solar" for the time zone category. |
809
|
|
|
|
|
|
|
|
810
|
|
|
|
|
|
|
=item is_utc() |
811
|
|
|
|
|
|
|
|
812
|
|
|
|
|
|
|
Returns 1 (true) if the time zone is equivalent to UTC, meaning at 0 offset from UTC. This is only the case for |
813
|
|
|
|
|
|
|
Solar/East00, Solar/West00 (which is an alias for Solar/East00), Solar/Lon000E and Solar/Lon000W (which is an alias |
814
|
|
|
|
|
|
|
for Solar/Lon000E). Otherwise it returns 0 because the time zone is not UTC. |
815
|
|
|
|
|
|
|
|
816
|
|
|
|
|
|
|
=item is_dst_for_datetime() |
817
|
|
|
|
|
|
|
|
818
|
|
|
|
|
|
|
always returns 0 (false) because Daylight Saving Time never occurs in Solar time zones. |
819
|
|
|
|
|
|
|
|
820
|
|
|
|
|
|
|
=item offset_for_datetime() |
821
|
|
|
|
|
|
|
|
822
|
|
|
|
|
|
|
returns the time zone's offset from UTC in seconds. This is equivalent to $obj->offset_sec(). |
823
|
|
|
|
|
|
|
|
824
|
|
|
|
|
|
|
=item offset_for_local_datetime() |
825
|
|
|
|
|
|
|
|
826
|
|
|
|
|
|
|
returns the time zone's offset from UTC in seconds. This is equivalent to $obj->offset_sec(). |
827
|
|
|
|
|
|
|
|
828
|
|
|
|
|
|
|
=item short_name_for_datetime() |
829
|
|
|
|
|
|
|
|
830
|
|
|
|
|
|
|
returns the time zone's short name, without "Solar/". This is equivalent to $obj->short_name(). |
831
|
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
=back |
833
|
|
|
|
|
|
|
|
834
|
|
|
|
|
|
|
I<TimeZone::Solar> also overloads the eq (string equality) and "" (convert to string) operators for |
835
|
|
|
|
|
|
|
compatibility with I<DateTime::TimeZone>. |
836
|
|
|
|
|
|
|
|
837
|
|
|
|
|
|
|
=head1 LICENSE |
838
|
|
|
|
|
|
|
|
839
|
|
|
|
|
|
|
I<TimeZone::Solar> is Open Source software licensed under the GNU General Public License Version 3. |
840
|
|
|
|
|
|
|
See L<https://www.gnu.org/licenses/gpl-3.0-standalone.html>. |
841
|
|
|
|
|
|
|
|
842
|
|
|
|
|
|
|
=head1 SEE ALSO |
843
|
|
|
|
|
|
|
|
844
|
|
|
|
|
|
|
LongitudeTZ on Github: https://github.com/ikluft/LongitudeTZ |
845
|
|
|
|
|
|
|
|
846
|
|
|
|
|
|
|
=head1 BUGS AND LIMITATIONS |
847
|
|
|
|
|
|
|
|
848
|
|
|
|
|
|
|
Please report bugs via GitHub at L<https://github.com/ikluft/LongitudeTZ/issues> |
849
|
|
|
|
|
|
|
|
850
|
|
|
|
|
|
|
Patches and enhancements may be submitted via a pull request at L<https://github.com/ikluft/LongitudeTZ/pulls> |
851
|
|
|
|
|
|
|
|
852
|
|
|
|
|
|
|
=head1 AUTHOR |
853
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
Ian Kluft <ian.kluft+github@gmail.com> |
855
|
|
|
|
|
|
|
|
856
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
857
|
|
|
|
|
|
|
|
858
|
|
|
|
|
|
|
This software is copyright (c) 2022 by Ian Kluft. |
859
|
|
|
|
|
|
|
|
860
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
861
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
862
|
|
|
|
|
|
|
|
863
|
|
|
|
|
|
|
=cut |