08 March 2024 | Challenge 259 |
Holiday Parser
Task 1: Banking Day Offset
Submitted by: Lee Johnson
You are given a start date and offset counter. Optionally you also get bank holiday date list.
Given a number (of days) and a start date, return the number (of days) adjusted to take into account non-banking days. In other words: convert a banking day offset to a calendar day offset.
Non-banking days are:
a) Weekends
b) Bank holidays
Example 1
Input: $start_date = '2018-06-28', $offset = 3, $bank_holidays = ['2018-07-03']
Output: '2018-07-04'
Thursday bumped to Wednesday (3 day offset, with Monday a bank holiday)
Example 2
Input: $start_date = '2018-06-28', $offset = 3
Output: '2018-07-03'
Solution
With next_business_day from Date::Manip::Date
comes a method that solves this task in almost a single call: it steps forward from a start date by n
business days (Mon - Fri) taking holidays into account.
So actually all that needs to be done is specifying the holidays.
Unfortunately these have to be provided in a configuration file.
Using a temporary file here.
use strict;
use warnings;
use Date::Manip::Date;
use File::Temp 'tempfile';
use experimental 'signatures';
sub bdo ($start, $offs, $bank=[]) {
my ($fh, $fn) = tempfile();
print $fh "*HOLIDAYS\n";
print $fh "$_ =\n" for @$bank;
close $fh;
my $sd = Date::Manip::Date->new($start);
$sd->config(ConfigFile => $fn);
$sd->next_business_day($offs);
$sd->printf('%Y-%m-%d');
}
See the full solution.
Task 2: Line Parser
Submitted by: Gabor Szabo
You are given a line like below:
{% id field1="value1" field2="value2" field3=42 %}
Where
a) "id" can be `\w+`.
b) There can be 0 or more field-value pairs.
c) The name of the fields are `\w+`.
d) The values are either number in which case we don't need double quotes or
string in which case we need double quotes around them.
The line parser should return structure like below:
{
name => id,
fields => {
field1 => value1,
field2 => value2,
field3 => value3,
}
}
It should be able to parse the following edge cases too:
{% youtube title="Title \"quoted\" done" %}
and
{% youtube title="Title with escaped backslash \\" %}
BONUS: Extend it to be able to handle multiline tags:
{% id field1="value1" ... %}
LINES
{% endid %}
You should expect the following structure from your line parser:
{
name => id,
fields => {
field1 => value1,
field2 => value2,
field3 => value3,
}
text => LINES
}
Solution
Using the regex engine to parse the given syntax.
Regexp::Common
generated patterns have a large number of unnamed capture groups that would be difficult to track if accessed by their global numbers.
Therefor wrapping these in (??{...})
blocks having their own numbering schema.
use strict;
use warnings;
use Regexp::Common qw(number delimited);
use Clone 'clone'
sub unescape {
shift =~ s{\\(.)}{$1}gr; # transform backslash escaped characters to
# themselves
}
sub line_parser {
our %tmp;
our $val;
my $parsed;
shift =~ m{
(?{ local %tmp }) # reset temporary parser result
\{% \s*+ # opening sequence '{%'
(?<NAME>\w++) # capture the name as NAME
(?{ $tmp{name} = $+{NAME}; }) # collect name in parser result
(?: # zero or more key-value pairs
\s++
(?<KEY>\w++)= # capture the key as KEY
(?{ local $val; }) # reset the value
(?:
(??{qr{$RE{num}{dec}{-keep} # match a decimal number
(?{$val = 0 + $1}) # store its value in $val
}x
})
| # or
# match a quoted string
(??{qr{$RE{delimited}{-delim => q{'"}}{-esc => '\\'}{-keep}
(?{$val = unescape($3)}) # store its content in $val
}x
})
)
(?{ $tmp{fields}{$+{KEY}} = $val; }) # collect key-value-pair
)*+
\s*+ %\} # closing sequence for first line
(?:
\n
(?<LINES>.*?) # capture optional multi lines
\n
(?{ $tmp{text} = $+{LINES}; }) # collect lines
\{% \s*+ # closing line for multi lines
end\g{NAME}
\s*+ %\}
)?
(?{ $parsed = clone \%tmp; }) # make the parser result permanent
}xs;
$parsed;
}
See the full solution.
If you have a question about this post or if you like to comment on it, feel free to open an issue in my github repository.