aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMohammad Sajid Anwar <Mohammad.Anwar@yahoo.com>2024-03-09 22:34:36 +0000
committerGitHub <noreply@github.com>2024-03-09 22:34:36 +0000
commit3a95281e37c0cdfa97b2c0af3aa9d32315ab5b71 (patch)
tree77014cc788e4f4c503c478e49f02c001d1d3f7db
parent3a447cfd06a4be8a2f5512a48a4804dac0f05582 (diff)
parent6f8f74356765e9d6ed331643d97d9402a8a69fa9 (diff)
downloadperlweeklychallenge-club-3a95281e37c0cdfa97b2c0af3aa9d32315ab5b71.tar.gz
perlweeklychallenge-club-3a95281e37c0cdfa97b2c0af3aa9d32315ab5b71.tar.bz2
perlweeklychallenge-club-3a95281e37c0cdfa97b2c0af3aa9d32315ab5b71.zip
Merge pull request #9712 from jeanluc2020/jeanluc-259
Add solution 259.
-rw-r--r--challenge-259/jeanluc2020/blog-1.txt1
-rw-r--r--challenge-259/jeanluc2020/blog-2.txt1
-rwxr-xr-xchallenge-259/jeanluc2020/perl/ch-1.pl80
-rwxr-xr-xchallenge-259/jeanluc2020/perl/ch-2.pl226
4 files changed, 308 insertions, 0 deletions
diff --git a/challenge-259/jeanluc2020/blog-1.txt b/challenge-259/jeanluc2020/blog-1.txt
new file mode 100644
index 0000000000..f8e5cb0616
--- /dev/null
+++ b/challenge-259/jeanluc2020/blog-1.txt
@@ -0,0 +1 @@
+http://gott-gehabt.de/800_wer_wir_sind/thomas/Homepage/Computer/perl/theweeklychallenge-259-1.html
diff --git a/challenge-259/jeanluc2020/blog-2.txt b/challenge-259/jeanluc2020/blog-2.txt
new file mode 100644
index 0000000000..85df72a6ca
--- /dev/null
+++ b/challenge-259/jeanluc2020/blog-2.txt
@@ -0,0 +1 @@
+http://gott-gehabt.de/800_wer_wir_sind/thomas/Homepage/Computer/perl/theweeklychallenge-259-2.html
diff --git a/challenge-259/jeanluc2020/perl/ch-1.pl b/challenge-259/jeanluc2020/perl/ch-1.pl
new file mode 100755
index 0000000000..d4544eae1d
--- /dev/null
+++ b/challenge-259/jeanluc2020/perl/ch-1.pl
@@ -0,0 +1,80 @@
+#!/usr/bin/env perl
+# https://theweeklychallenge.org/blog/perl-weekly-challenge-259/#TASK1
+#
+# Task 1: Banking Day Offset
+# ==========================
+#
+# 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'
+#
+############################################################
+##
+## discussion
+##
+############################################################
+#
+# As long as offset is > 0, we add one to the current date. If the
+# new date is neither a bank holiday nor on a weekend, we decrease
+# the remaining offset by one.
+# I use Date::Calc to take care of the date calculation part.
+
+use strict;
+use warnings;
+use Date::Calc qw(Day_of_Week Add_Delta_Days);
+
+banking_day_offset('2018-06-28', 3, ['2018-07-03']);
+banking_day_offset('2018-06-28', 3);
+
+sub banking_day_offset {
+ my ($start_date, $offset, $bank_holidays) = @_;
+ $bank_holidays //= [];
+ print "Input: start_date = $start_date, offset = $offset, bank_holidays = [" . join(", ", @$bank_holidays) . "]\n";
+ while($offset > 0) {
+ my $next_date = next_day($start_date);
+ if( is_bankholiday_or_weekend($next_date, $bank_holidays) ) {
+ $start_date = $next_date;
+ next;
+ }
+ $offset--;
+ $start_date = $next_date;
+ }
+ print "Output: $start_date\n";
+}
+
+sub is_bankholiday_or_weekend {
+ my ($date, $bank_holidays) = @_;
+ return 1 if Day_of_Week(split(/-/, $date)) > 5;
+ foreach my $d (@$bank_holidays) {
+ return 1 if $d eq $date;
+ }
+ return 0;
+}
+
+sub next_day {
+ my $date = shift;
+ my ($year, $month, $day) = split /-/, $date;
+ my @new_date = Add_Delta_Days($year, $month, $day, 1);
+ my $d = sprintf("%04d-%02d-%02d", @new_date);
+ return $d;
+}
diff --git a/challenge-259/jeanluc2020/perl/ch-2.pl b/challenge-259/jeanluc2020/perl/ch-2.pl
new file mode 100755
index 0000000000..8a66ffe2c3
--- /dev/null
+++ b/challenge-259/jeanluc2020/perl/ch-2.pl
@@ -0,0 +1,226 @@
+#!/usr/bin/env perl
+# https://theweeklychallenge.org/blog/perl-weekly-challenge-259/#TASK2
+#
+# Task 2: Line Parser
+# ===================
+#
+# 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+.
+# b) 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 filed1="value1" ... %}
+# LINES
+# {% endid %}
+#
+# You should expect the following structure from your line parser:
+#
+# {
+# name => id,
+# fields => {
+# field1 => value1,
+# field2 => value2,
+# field3 => value3,
+# }
+# text => LINES
+# }
+#
+############################################################
+##
+## discussion
+##
+############################################################
+#
+# Parsers are always a bit complicated, so most solutions to
+# this problem will probably be as well.
+# There might also always be some corner cases that are not
+# clear right away. In this case the description says:
+# 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.
+# It remains unclear whether or not this includes floating point
+# numbers or only integers, so I decided to at least also support
+# decimal number written with a single "." like 12.34, but not
+# the full list of possible representations of floats like 1E5 or
+# similar stuff.
+# A full parser usually needs to split everything into tokens which
+# can then be processed one by one, and regular expressions are not
+# enough to build a full parser in most cases. In our case, I used
+# a mixed approach: handle everything with regular expressions
+# that is easy to handle that way. However, parsing strings is done
+# by handing in the rest of the current line into a function that
+# will then return the string at the beginning of the line and the
+# remainder of the input once the string is removed. This function
+# parses the rest of the line one character at a time, no regular
+# expressions there. The loop that calls this function goes line
+# at a time, always picking up larger chunks of the line by using
+# regular expressions. This outer loop makes use of the Switch
+# module unnecessarily as my first intention was to implement a
+# full character-by-character parser which would have required
+# some kind of a state machine to always know where in the parsing
+# process we are at any time, but then I decided to try an
+# approach making use of regular expressions, so a single variable
+# could have taken care of everything (Just keep the sectionname
+# in a variable, and if empty assume we're in the default state
+# and take it from there), but then I wouldn't have had the
+# opportunity to try out the Switch module, so I kept the code as
+# it was.
+
+use strict;
+use warnings;
+use Switch;
+use Data::Dumper;
+
+my $DEFAULT = 'Default';
+my $INSECTION = 'InSection';
+my $SECTIONNAME = "";
+
+my @ALL_DATA; # the result will be stored here
+my $CURRENT_DATA = {}; # for temporarily storing the data we're currently parsing
+my $state = $DEFAULT; # initialize the state machine
+
+my $input = <<EOF;
+{% id field1="value1" field2="value2" field3=42 %}
+{% endid %}
+Some random data somewhere in between
+{% youtube title="Title \\"quoted\\" done" %}
+{% endyoutube %}
+{% youtube title="Title with escaped backslash \\\\" %}
+Some data
+More data
+{% endyoutube %}
+{% id field1="value1" field2="value2" field3=42.32 field4="Hello, world!" %}
+{% endid %}
+{% foo %}
+Bar.
+{% endfoo %}
+EOF
+my @lines = split /\n/, $input;
+
+foreach my $line (@lines) {
+ switch($state) {
+ case "$DEFAULT" {
+ # not inside a section, so we expect either the begin of a new section
+ # or a plain string to ingest
+ if($line =~ m/^\s*\{\%\s*(\w+)/) { # new section starts
+ # keep name of section and switch to correct state
+ $SECTIONNAME = $1;
+ $state = $INSECTION;
+ # remove start of line including the section name
+ $line =~ s/\s*\{\%\s*$SECTIONNAME\s+//;
+ # initialize temporary data structure's name
+ $CURRENT_DATA->{name} = $SECTIONNAME;
+ while(length($line) > 0) { # ingest rest of line
+ # remove unnecessary whitespace
+ $line =~ s/^\s+//;
+ if($line =~ m/^\s*\%\}\s*$/) {
+ # we found the end of the line, let's just set everything
+ # into a state that will break out of the loop.
+ $line = "";
+ } else {
+ # now we have a new field at the beginning of the line, so
+ # we remove (and capture) the field name
+ $line =~ s/^(\w+)=//;
+ my $key = $1;
+ if($line =~ m/^(\d+)(\.\d+){0,1}(\s|\%\})/) {
+ # number at beginning of line, remove it and
+ # capture it in the temporary data structure
+ $line =~ s/^(\d+(\.\d+){0,1})//;
+ $CURRENT_DATA->{fields}->{$key} = $1;
+ } elsif ( $line =~ m/^"/ ) {
+ # string at the beginning of the line, let's hand the
+ # function into the parser function for this case, then
+ # store the resulting string into the temporary data
+ # structure, keeping the remainder in $line for further
+ # processing in the next iteration of the loop
+ my $string;
+ ($string, $line) = parse_next_string($line);
+ $CURRENT_DATA->{fields}->{$key} = $string;
+ } else {
+ die "Parse error, value neither number nor string here ->$line!";
+ }
+ }
+ }
+ } else { # plain string to ingest
+ push @ALL_DATA, $line;
+ }
+ }
+ case "$INSECTION" {
+ # either we find the end of the current section, or we copy the line as-is
+ # to the text inside our data structure
+ if($line =~ m/^\s*\{\%\s*end$SECTIONNAME\s*\%\}\s*$/) {
+ # end of section: store current temporary data into the result,
+ # reset the temporary data structure and switch back to the default
+ # state
+ push @ALL_DATA, $CURRENT_DATA;
+ $CURRENT_DATA = {};
+ $state = $DEFAULT;
+ } else {
+ $CURRENT_DATA->{text} .= "$line\n";
+ }
+ }
+ else { die "Unknown state $state!\n"; }
+ }
+}
+foreach my $entry (@ALL_DATA) {
+ print Dumper $entry;
+}
+
+# given the rest of a line, split this into the string at the beginning
+# of the line and the remainder of the line after removing this string
+sub parse_next_string {
+ my $input = shift;
+ my @chars = split //, $input;
+ my ($string, $rest) = ("", "");
+ die "Not a string" unless $chars[0] eq '"';
+ my $index = 1;
+ while($index <= $#chars) {
+ if($chars[$index] eq '"') { # closing '"', finish everything up
+ $rest = substr($input, $index+1);
+ return ($string, $rest);
+ } elsif ($chars[$index] eq "\\") {
+ # if we find a '\', we just ingest the next character as is.
+ # This means '\\' turns into a single '\', '\"' turns into
+ # a single '"', and other stuff like '\a' turns into 'a'.
+ # No need to make things more complicated in this case.
+ $index++;
+ $string .= $chars[$index];
+ } else {
+ # everything apart from '\' or '"' just indicates a character
+ # to be ingested as-is.
+ $string .= $chars[$index];
+ }
+ $index++;
+ }
+ # if we end up here, there was no matching '"'
+ # so we ran into a parsing error
+ die "Parser error: Could not find closing '\"' in $input";
+}