DateTim...Tutorial(3)

πŸ’… DateTim...Tutorial(3) β€” like, the DateTime::Format::Builder Tutorial, bestie

okay so this is a User Contributed Perl Documentation page for DateTime::Format::Builder::Tutorial and it's literally a quick class on using Builder. grab a strawberry drink πŸ“ and let's go.


✨ NAME ✨

DateTime::Format::Builder::Tutorial β€” a quick class on using Builder, no cap.

πŸŽ€ VERSION πŸŽ€

version 0.83 β€” that's the one, periodt.


🌸 CREATING A CLASS 🌸

so, like, as most people who write modules already know (iykyk), you start a package with a package declaration and some indication of module version:

package DateTime::Format::ICal; our $VERSION = '0.04';

after that, bestie, you call Builder with some options. there are only a few of them (they get detailed later, don't worry). right now we're only interested in parsers.

use DateTime::Format::Builder ( parsers => {...} );

the parsers option takes a reference to a hash of method names and specifications:

parsers => {
        parse_datetime => ... ,
        parse_datetime_with_timezone => ... ,
        ...
    }

Builder will create methods in your class, and each method is a parser that follows the given specifications. it is strongly recommended that one method is called parse_datetime β€” like, whether it's a Builder-created method or one of your own. 🫢

in addition to creating any of the parser methods, it also creates a new method that can instantiate (or clone) objects of this class. this behaviour can be modified with the constructor option, but we don't need to know that yet. πŸ™ˆ

each value corresponding to a method name in the parsers list is either a single specification, or a list of specifications. we'll start with the simple case, obviously:

parse_briefdate => { params => [qw( year month day )], regex =>
    qr/^(\d\d\d\d)(\d\d)(\d\d)$/, },

this will result in a method named parse_briefdate which takes strings in the form 20040716 and returns DateTime objects representing that date. a user of the class might write:

use DateTime::Format::ICal;
my $date = '19790716';
my $dt   = DateTime::Format::ICal->parse_briefdate($date);
print "My birth month is ", $dt->month_name, "\n";

the regex is applied to the input string, and if it matches, then $1, $2, ... are mapped to the params given and handed to DateTime->new. essentially:

my $rv = DateTime->new( year => $1, month => $2, day => $3 );

there are more complicated things one can do within a single specification, but we'll cover those later. πŸ‘‘


often you'll want a method to be able to take one string and run it against multiple parser specifications. like, it would be very irritating if the user had to work out what format the datetime string was in and then which method was most appropriate. so annoying. 😭

so Builder lets you specify multiple specifications:

parse_datetime => [
    {
        params => [qw( year month day hour minute second )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)$/,
    },
    {
        params => [qw( year month day hour minute )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)$/,
    },
    {
        params => [qw( year month day hour )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)$/,
    },
    {
        params => [qw( year month day )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)$/,
    },
],

it's an arrayref of specifications. a parser will be created that will try each of these specifications sequentially, in the order you specified. slay.

there's a flaw with this though. πŸ’€ in this example we're building a parser for ICal datetimes. one can place a timezone id at the start of an ICal datetime. you might extract such an id with the following code:

if ( $date =~ s/^TZID=([^:]+):// ) {
    $time_zone = $1;
}

# Z at end means UTC
elsif ( $date =~ s/Z$// ) {
    $time_zone = 'UTC';
}
else {
    $time_zone = 'floating';
}

$date would end up without the id, and $time_zone would contain something appropriate to give to DateTime's set_time_zone method, or time_zone argument.

but like... how do you get this scrap of code into your parser? you might be tempted to call the parser something else and build a small wrapper. there's no need though, because an option is provided for preprocessing dates:

parse_datetime => [
    [ preprocess => \&_parse_tz ],    # Only changed line!
    {
        params => [qw( year month day hour minute second )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)$/,
    },
    {
        params => [qw( year month day hour minute )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)$/,
    },
    {
        params => [qw( year month day hour )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)$/,
    },
    {
        params => [qw( year month day )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)$/,
    },
],

it will necessitate _parse_tz to be written, and that routine looks like this:

sub _parse_tz {
    my %args = @_;
    my ( $date, $p ) = @args{qw( input parsed )};
    if ( $date =~ s/^TZID=([^:]+):// ) {
        $p->{time_zone} = $1;
    }

    # Z at end means UTC
    elsif ( $date =~ s/Z$// ) {
        $p->{time_zone} = 'UTC';
    }
    else {
        $p->{time_zone} = 'floating';
    }
    return $date;
}

on input it is given a hash containing two items: the input date, and a hashref that will be used in the parsing. the return value from the routine is what the parser specifications will run against, and anything in the parsed hash ($p in the example) will be put in the call to DateTime->new(...). 🧚


so we now have a happily working ICal parser. it parses the assorted formats, and can also handle timezones. is there anything else it needs to do? no. but we can make it work more efficiently. ✨

at present, the specifications are tested sequentially. however, each one applies to strings of particular lengths. thus we could be efficient and have the parser only test the given strings against a parser that handles that string length. again, Builder makes it easy:

parse_datetime => [
    [ preprocess => \&_parse_tz ],
    {
        length => 15,    # We handle strings of exactly 15 chars
        params => [qw( year month day hour minute second )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)$/,
    },
    {
        length => 13,                                # exactly 13 chars...
        params => [qw( year month day hour minute )],
        regex => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)$/,
    },
    {
        length => 11,                                    # 11..
        params => [qw( year month day hour )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)$/,
    },
    {
        length => 8,                                     # yes.
        params => [qw( year month day )],
        regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)$/,
    },
],

now the created parser will create a parser that only runs specifications against appropriate strings. main character energy, honestly.

so our complete code looks like:

package DateTime::Format::ICal;
use strict;
our $VERSION = '0.04';

use DateTime::Format::Builder (
    parsers => {
        parse_datetime => [
            [ preprocess => \&_parse_tz ],
            {
                length => 15,
                params => [qw( year month day hour minute second )],
                regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)$/,
            },
            {
                length => 13,
                params => [qw( year month day hour minute )],
                regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)$/,
            },
            {
                length => 11,
                params => [qw( year month day hour )],
                regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)T(\d\d)$/,
            },
            {
                length => 8,
                params => [qw( year month day )],
                regex  => qr/^(\d\d\d\d)(\d\d)(\d\d)$/,
            },
        ],
    },
);

sub _parse_tz {
    my %args = @_;
    my ( $date, $p ) = @args{qw( input parsed )};
    if ( $date =~ s/^TZID=([^:]+):// ) {
        $p->{time_zone} = $1;
    }

    # Z at end means UTC
    elsif ( $date =~ s/Z$// ) {
        $p->{time_zone} = 'UTC';
    }
    else {
        $p->{time_zone} = 'floating';
    }
    return $date;
}

1;

and that's an ICal parser. πŸŽ‰ the actual DateTime::Format::ICal module also includes formatting methods and parsing for durations, but Builder doesn't support those yet. a drop in replacement (at the time of writing the replacement) can be found in the examples directory of the Builder distribution, along with similar variants of other common modules. obsessed.


πŸ¦‹ SEE ALSO πŸ¦‹

πŸ”₯ SUPPORT πŸ”₯

bugs may be submitted at <https://github.com/houseabsolute/DateTime-Format-Builder/issues>. go off, file that bug.

i am also usually active on IRC as 'autarch' on "irc://irc.perl.org". come say hi 🌈

πŸ‘‘ SOURCE πŸ‘‘

the source code repository for DateTime-Format-Builder can be found at <https://github.com/houseabsolute/DateTime-Format-Builder>.

πŸ’œ AUTHORS πŸ’œ

  • Dave Rolsky <autarch@urth.org>
  • Iain Truskett <spoon@cpan.org>

this software is Copyright (c) 2020 by Dave Rolsky.

this is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)

the full text of the license can be found in the LICENSE file included with this distribution.


ngl bestie, read the license before you ship it β€” it's giving legally binding. πŸ’…

okay that's the whole page!! mwah πŸ’– β€” this was perl v5.40.0, dated 2023-01-15, brought to you by User Contributed Perl Documentation, and it's DateTim...Tutorial(3) signing off 🫢✨

πŸ“¦ where this page comes from