Хорошим такое API никак не назовешь. Самое время задуматься над созданием удобного для использования класса.$ perl julian.pl 1975 2 29
JD: 2442472.500000
JD at 0h: 2442471.500000
Fri
$ perl julian.pl 1975 3 1
JD: 2442472.500000
JD at 0h: 2442471.500000
Fri
В одних случаях нужно получать из календарной даты юлианскую, в других -- наоборот. Поэтому объект должен инициализироваться как из первой, так из второй даты. Если на входе год, месяц и число, то они сохраняются, а юлианская дата неопределена (undef) и вычисляется лениво, т.е только при вызове метода djd. Точно так же, если объект создан с аргументом djd, календарная дата неопределена до тех пор, пока ее не запросили.
В Perl-е не существует перегрузки методов, следовательно, нельзя создать два разных конструктора: один -- с календарной датой на входе, другой с юлианской. Можно было бы передавать в конструктор new именованные аргументы, что-то вроде:
Но тогда пришлось бы нагромождать проверки: не заданы ли сразу оба аргумента и какой именно задан. А потом еще проверять правильность либо первого либо второго. Проще создать два "фабричных" метода:Julian->new(date => \%ymd, djd => $scalar)
new_from_date(year => $scalar, month => $scalar, day => $scalar)
new_from_djd($djd)
Как правило, я стараюсь не использовать модуль Params::Validate, который создан как раз для проверки входных параметров -- уж больно велики накладные расходы, да и ни к чему превращать динамический язык в подобие Java. Но тут как раз тот случай, когда без него не обойтись.
Начнем с более простого метода new_from_djd.
Поскольку параметры здесь не именованные, а позиционные, для валидации данных используется метод validate_pos из пакета Params::Validate. Имя класса предварительно убрано из списка аргументов ( @_ ). Проверка производится по трем критериям:...
use Params::Validate qw/:all/;
use Readonly;
Readonly our $DJD_TO_JD => 2415020;
...
sub new_from_djd {
my $class = shift;
validate_pos(
@_,
{
type => SCALAR,
regex => qr/^[-+]?0*(\d+|(?:\d*\।\d+))$/,
callbacks => {
'djd range' => sub{ $_[0] >= -$DJD_TO_JD }
}
}
);
bless {
_djd => shift,
_date => undef,
}, $class
}
...
type => SCALAR
аргумент должен быть скаляромqr/^[-+]?0*(\d+|(?:\d*\.\d+))$/
аргумент должен быть целым или десятичным числом, с любым количеством ведущих нулей и с необязательным ведущим "плюсом" или "минусом"'djd range' => sub{ $_[0] >= -$DJD_TO_JD }
число не должно быть меньше нулевой стандартной юлианской даты (напомню, что видоизмененная юлианская дата, которую мы используем, больше стандартной на 2415020 суток. Функции обратного вызова (callbacks) -- "тяжелая артиллерия" пакет Params::Validate. Внутри них можно осуществлять любые проверки. На входе всегда два параметра: проверяемый аргумент и ссылка на все аргументы (хэш или массив).
Второй метод-фабрика сложнее:
Здесь аргументы именованные, поэтому для их валидации используется метод validate.
...
use Params::Validate qw/:all/;
use Readonly;
Readonly our $DJD_TO_JD => 2415020;
Readonly our $MIN_YEAR => -4713;
Readonly our $MAX_YEAR => 4713;
...
sub new_from_date {
my $class = shift;
my %args = validate(
@_,
{
year => {
type => SCALAR,
regex => qr/^[\-+]?\d{1,4}$/,
callbacks => {
'year range' => sub{
$MIN_YEAR <= $_[0] && $MAX_YEAR >= $_[0]
},
'non-zero year' => sub{ 0 != $_[0] }
}
},
month => {
type => SCALAR,
regex => qr/^0*([1-9]|(1[0-2]))$/,
},
day => {
type => SCALAR,
regex => qr/^0*([1-9]|([0-2]\d)|(3[0,1]))(\।\d+)?$/,
callbacks => {
'day range' => sub{
my ($d, $arg) = @_;
$d >= 1 &&
$d < _days_per_month($arg->{year}, $arg->{month}) + 1;
}
}
},
}
);
bless {
_djd => undef,
_date => \%args,
}, $class
}
...
- год проверяется по четырем критериям:
- тип аргумента -- скаляр
- аргумент -- положительное или отрицательное целое число
- диапазон: от 4713г. до н.э до 4713г. н.э.
- ноль не допускается
- У месяца тип должен должен быть скаляром, а диапазон (1-12) проверяется регулярным выражением
qr/^0*([1-9]|(1[0-2]))$/
- Проверка числа самая сложная.
- тип аргумента -- скаляр.
- при помощи регулярного выражения проверяем, что это число, возможно, с десятичными долями; при этом целая часть не выходит за рамки диапазона 1-31.
- При окончательной проверке диапазона вызывается внешняя функция _days_per_month (см. ниже)
- тип аргумента -- скаляр.
Сколько дней в месяце?
Количество дней во всех месяцах, кроме февраля, в григорианском календаре неизменно.Чтобы узнать число дней в феврале, надо определить, является ли год високосным. Если да -- то 29, если нет -- то 28. Для определения "високосности" функция _leap_year. Год в григориансмком календаре является високосным, если он кратен 4 и при этом не кратен 100, либо кратен 400 (в отличие от "старого стиля", где високосным считался каждый четвертый год).
# является ли год високосным?
sub _leap_year {
my $y = shift;
return 1 if $y % 400 == 0;
return 0 if $y % 100 == 0;
return 1 if $y % 4 == 0;
return 0;
}
# количество дней в месяце
sub _days_per_month {
my ($y, $m) = @_;
return _leap_year($y) ? 29 : 28 if $m == 2; # февраль
return 30 if grep{ $m == $_ } (4, 6, 9, 11); # апрель, июнь, сентябрь, ноябрь
return 31; # остальные месяцы
}
Ленивые вычисления
Два getter-а: djd и date служат для получения юлианской и календарной даты. Если либо одно либо другое не определено (undef), оно вычисляется и сохраняется как аттрибут объекта.Функции _date2djd и _djd2date, которые заняты вычислениями, уже описаны в предыдущей заметке. Здесь я только добавил к их названиям нижнее подчеркивания в качестве рекомендации не использовать их снаружи.sub djd {
my $self = shift;
$self->{_djd} = _date2djd(%{$self->{_date}})
unless defined $self->{_djd};
$self->{_djd}
}
sub date {
my $self = shift;
$self->{_date} = _djd2date($self->{_djd})
unless defined $self->{_date};
$self->{_date}
}
Комментариев нет:
Отправить комментарий