Perl Weekly Challenge 259: too much work to do!

This post presents my solutions to the Perl Weekly Challenge 259.
I keep doing the Perl Weekly Challenge in order to mantain my coding skills in good shape, as well as in order to learn new things, with particular regard to Raku, a language that I love.
This week, I solved the following tasks:
The PL/Perl implementations are very similar to a pure Perl implementation, even if the PostgreSQL environment could involve some more constraints. Similarly, the PL/PgSQL implementations help me keeping my PostgreSQL programming skills in good shape.

Raku Implementations

PWC 259 - Task 1 - Raku Implementation

The first task was about computing a destination date given an initial date, an offset (in days) and a set of dates to skip. THe script has to compute the final date skipping weekends and all the days into the optionally specified list of holidays.

sub MAIN( $date,
	  Int $offset is copy,
	  *@holidays
				where { $date ~~ / ^ \d ** 4 <[-]>\d ** 2 <[-]> \d ** 2 $ / and $offset > 0 } ) {

    my $day = DateTime.new( $date, formatter => { sprintf "%04d-%02d-%02d", .year, .month, .day } );


    while ( $offset > 0 ) {
		$day .= later( days => 1 );

		# skip weekends
		while ( $day.day-of-week == any( 7, 6 ) ) {
		    $day .= later( days => 1 );
		}

		while ( @holidays.elems > 0 && @holidays.grep( * ~~ $day.Str ) ) {
		    $day .= later( days => 1 );
		}


		$offset--;
    }

    $day.say;

}



The idea is to store the DateTime object into a $day variable that is increased by one day at a time (by means of later) looping for every $offset days specified. The $day is moved forward one day at a time if it is a weekend or is found in the holidays array.

PWC 259 - Task 2 - Raku Implementation

The task asked to write a parser for a line of text in a form with an identifier and a set of params (key and value). I have not implemented this task in a very accurate way, since it is too much regular expression work for me!

sub MAIN() {

    my %parsed;

	my $line = '...';

    my regex id { \w+ };
    my regex option {  $<name>= [ \w+ ] \s* <[=]> $<value>= [ \w+ | \" \w+ \s* .* \" ] };
    my regex opening { <[{]> <[%]> };
    my regex closing { <[%]> <[}]> };

    if ( $line ~~ / ^ <opening> \s+ <id> \s+ <option>* % \s  \s* <closing> $ / ) {

		%parsed<name> = $/<id>;

		say $/<option>;
		for $/<option> {
		    my ( $key, $value ) = .split( '=' );
		    %parsed<fields>{ $key } = $value;
		}
    }

    say %parsed;
}



PL/Perl Implementations

PWC 259 - Task 1 - PL/Perl Implementation

Similar implementation to the Raku one, with DateTime object used for days.

CREATE OR REPLACE FUNCTION
pwc259.task1_plperl( date, int, date[] )
RETURNS date
AS $CODE$

use DateTime;

   my ( $when, $offset, $holidays ) = @_;

   $when =~ / ^ (?<year>\d{4}) [-] (?<month>\d{2}) [-] (?<day>\d{2}) $ /x;

   my $day = DateTime->new( year => $+{ year}, month => $+{ month }, day => $+{ day }	 );

   while ( $offset > 0 ) {
   	 $day->add( days => 1 );
	 $offset--;

	 # skip weekends
	 while ( $day->day_of_week == 6 || $day->day_of_week == 7 ) {
	       $day->add( days => 1 );
	 }

	 if ( $holidays->@* ) {
	    while( grep { $_ eq $day->ymd } $holidays->@* ) {
	       $day->add( days => 1 );
	    }
	 }
   }

   return $day->ymd;

$CODE$
LANGUAGE plperlu;



PWC 259 - Task 2 - PL/Perl Implementation

Again, a disaster of regular expressions! This time I tried to implement a kind of staging parser, where each character of the paramaters string is analyzed to understand if accumulating a key or a value for a pair.

CREATE OR REPLACE FUNCTION
pwc259.task2_plperl( text)
RETURNS TABLE( id text, field_name text, field_value text )
AS $CODE$

   my ( $line ) = @_;

   if ( $line =~ / ^ [{] [%] \s* (?<id>\w+) \s* (?<options>.*) \s* [%] [}] $ /x ) {
      my $id = $+{ id };
      my ( $name, $value ) = ( '', '' );
	if ( $+{ options } ) {
	   my $is_value = 0;
	   my $allowed_spaces = 0;
	   my $previous = '';

	   for ( split //, $+{ options } ) {


	       $is_value = 1 and $previous = $_ and next if ( $_ eq '=' );
	       $allowed_spaces = 1 and $previous = $_ and next if ( $_ eq '"' and $previous eq '=' );

	       $name .= $_ if ( ! $is_value );
	       $value .= $_ if ( $is_value );

	       if ( $is_value
	           && ( ( $_ eq ' ' && ! $allowed_spaces )
		     || ( $_ eq '"' && $previous ne '\\' && $allowed_spaces ) )
		     ) {
	       	  # stop here!
		  $value =~ s/^\s*|\s*$//g;
		  $value =~ s/^["]|["]$//g;
		  $value =~ s/\\"/"/g;
     		  return_next( { id => $id ,
      		     field_name => $name,
		     field_value => $value } );

		  ( $name, $value, $is_value, $allowed_spaces ) = ( '', '', 0, 0 );
	       }

	       $previous = $_;
	   }


	   }
   }

   return undef;

$CODE$
LANGUAGE plperl;



PostgreSQL Implementations

PWC 259 - Task 1 - PL/PgSQL Implementation

Quite simple to implement, using the same approach of PL/Perl:

CREATE OR REPLACE FUNCTION
pwc259.task1_plpgsql( day date, how_many_days int, holidays date[] )
RETURNS date
AS $CODE$
DECLARE
	current_holiday date;
BEGIN

	WHILE how_many_days > 0 LOOP
	      day := day + 1;

	      WHILE extract( dow from day ) IN ( 0, 6 ) LOOP
	      	 day := day + 1;
              END LOOP;

	      FOREACH current_holiday IN ARRAY holidays LOOP
	      	      IF current_holiday = day THEN
		      	 day := day + 1;
		      END IF;
	      END LOOP;

	      how_many_days := how_many_days - 1;
	END LOOP;

	RETURN day;
END
$CODE$
LANGUAGE plpgsql;



PWC 259 - Task 2 - PL/PgSQL Implementation

Here I faked! I used the PL/Perl (not complete) implementation, since there is too much work to do in PL/PgSQL with this regular expressions.

CREATE OR REPLACE FUNCTION
pwc259.task2_plpgsql( line text )
RETURNS TABLE( id text, field_name text, field_value text )
AS $CODE$
   SELECT pwc259.task2_plperl( line );
$CODE$
LANGUAGE sql;



Java Implementations

PWC 259 - Task 1 - PostgreSQL PL/Java Implementation

Same implementation as PL/Perl and the others, with the care of using java.sql.Date objects.

public class Task1 {

    private final static Logger logger = Logger.getAnonymousLogger();

    @Function( schema = "pwc259",
	       onNullInput = RETURNS_NULL,
	       effects = IMMUTABLE )
    public static final java.sql.Date task1_pljava( Date startDay, int how_many, Date[] holidays ) throws SQLException {
		logger.log( Level.INFO, "Entering pwc259.task1_pljava" );

		Calendar day = Calendar.getInstance();
		day.setTime( startDay );

		while ( how_many > 0 ) {
		    day.add( Calendar.DAY_OF_YEAR, 1 );

		    while ( day.get( Calendar.DAY_OF_MONTH ) == Calendar.SUNDAY
			    || day.get( Calendar.DAY_OF_MONTH ) == Calendar.SATURDAY )
			day.add( Calendar.DAY_OF_YEAR, 1 );

		    if ( holidays != null )
			for ( Date skip : holidays )
			    if ( skip.equals( day.getTime() ) )
				day.add( Calendar.DAY_OF_YEAR, 1 );

		    how_many--;
		}

		return new java.sql.Date( day.getTimeInMillis() );
    }
}



PWC 259 - Task 2 - PostgreSQL PL/Java Implementation

The implementation of this task has been done partially, using nested regular expressions.

public class Task2 implements ResultSetProvider {

    private final static Logger logger = Logger.getAnonymousLogger();

    @Function( schema = "pwc259",
	       onNullInput = RETURNS_NULL,
	       effects = IMMUTABLE )
    public static final ResultSetProvider task2_pljava( String line ) throws SQLException {
		return new Task2( line );
    }


    public Task2( String line ) throws SQLException {
		params = new LinkedList< List<String> >();
		parse( line );
    }


    private final void parse( String line ) throws SQLException {
		Pattern pattern = Pattern.compile( "[{][%] (\\w+)\\s*(.*)\\s* [%][}]" );
		Matcher matcher = pattern.matcher( line );

		if ( matcher.find() ) {
		    id = matcher.group( 1 );

		    Pattern subPattern = Pattern.compile( "(\\w+)[=](\\w+)\\s*" );
		    Matcher subMatch   = subPattern.matcher( matcher.group( 2 ) );

		    while ( subMatch.find() ) {
			List<String> current = new LinkedList<String>();
			current.add( subMatch.group( 1 ) );
			current.add( subMatch.group( 2 ) );
			params.add( current );

		    }
		}
		else
		    throw new SQLException( "Cannot parse " + line );

    }

    private List< List<String> > params;
    private String id;

    @Override
    public boolean assignRowValues(ResultSet rs, int row)
	throws SQLException {

		// stop the result set
		if ( row >= params.size() )
		    return false;

		rs.updateInt( 1, row );
		rs.updateString( 2, id );
		rs.updateString( 3, params.get( row ).get( 0 ) );
		rs.updateString( 4, params.get( row ).get( 1 ) );
		return true;
    }

    @Override
    public void close() {
    }
}



The idea is to perform the parsing within the parse() method, that will store the parameters into the params list, so that the function can return such list as a result set.

Python Implementations

PWC 259 - Task 1 - Python Implementation

Same implementation as in other tasks, but requires a timedelta to add one day at a time!

import sys
from datetime import date, timedelta

# task implementation
# the return value will be printed
def task_1( args ):
    day      = date.fromisoformat( args[ 0 ] )
    offset   = int( args[ 1 ] )
    holidays = list( map( lambda x: date.fromisoformat( x ), args[ 2: ] ) )
    one_day  = timedelta( days=1 )

    while offset > 0 :
        day += one_day

        while day.weekday() >= 6 or day.weekday() == 0:
            day += one_day

        while day in holidays :
            day += one_day

        offset -= 1

    return day


# invoke the main without the command itself
if __name__ == '__main__':
    print( task_1( sys.argv[ 1: ] ) )



PWC 259 - Task 2 - Python Implementation

A character-at-a-time parser, not a very good implementation, sob!

import sys

# task implementation
# the return value will be printed
def task_2( args ):
    line = args[ 0 ]

    parsed = {}
    id = ''
    key = ''
    value = ''
    is_value = False
    is_key  = False

    for c in line:
        if c == '{' :
            continue
        elif c == '%':
            continue
        elif c == '}':
            return parsed
        elif c == ' ':
            if not 'id' in parsed and len( id ) > 0 :
                parsed[ 'id' ] = id
                parsed[ 'fields' ] = []
                is_key = True
                continue
            elif 'fields' in parsed:
                parsed[ 'fields' ].append( { key : value } )
                is_key = True
                key = ''
                value = ''
                is_value = False

        elif c != '=' :
            if not 'id' in parsed:
                id += c
                is_value = False
                is_key = False
                continue
            else:
                if is_key:
                    key += c
                else:
                    value += c

        elif c == '=':
            if is_key:
                is_value = True
                is_key   = False




    return parsed


# invoke the main without the command itself
if __name__ == '__main__':
    print( task_2( sys.argv[ 1: ] ) )



The article Perl Weekly Challenge 259: too much work to do! has been posted by Luca Ferrari on March 4, 2024