site.base_url }}/">The Bear's Den -->

The Bear's Den

Enter at your own risk

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.