package Venus::Match;
use 5.018;
use strict;
use warnings;
use Venus::Class 'attr', 'base', 'with';
base 'Venus::Kind::Utility';
with 'Venus::Role::Valuable';
with 'Venus::Role::Buildable';
with 'Venus::Role::Accessible';
use Scalar::Util ();
# ATTRIBUTES
attr 'on_none';
attr 'on_only';
attr 'on_then';
attr 'on_when';
# BUILDERS
sub build_self {
my ($self, $data) = @_;
$self->on_none(sub{}) if !$self->on_none;
$self->on_only(sub{1}) if !$self->on_only;
$self->on_then([]) if !$self->on_then;
$self->on_when([]) if !$self->on_when;
return $self;
}
# METHODS
sub clear {
my ($self) = @_;
$self->on_none(sub{});
$self->on_only(sub{1});
$self->on_then([]);
$self->on_when([]);
return $self;
}
sub data {
my ($self, $data) = @_;
while(my($key, $value) = each(%$data)) {
$self->just($key)->then($value);
}
return $self;
}
sub expr {
my ($self, $topic) = @_;
$self->when(sub{
my $value = $self->value;
if (!defined $value) {
return false;
}
if (Scalar::Util::blessed($value) && !overload::Overloaded($value)) {
return false;
}
if (!Scalar::Util::blessed($value) && ref($value)) {
return false;
}
if (ref($topic) eq 'Regexp' && "$value" =~ qr/$topic/) {
return true;
}
elsif ("$value" eq "$topic") {
return true;
}
else {
return false;
}
});
return $self;
}
sub just {
my ($self, $topic) = @_;
$self->when(sub{
my $value = $self->value;
if (!defined $value) {
return false;
}
if (Scalar::Util::blessed($value) && !overload::Overloaded($value)) {
return false;
}
if (!Scalar::Util::blessed($value) && ref($value)) {
return false;
}
if ("$value" eq "$topic") {
return true;
}
else {
return false;
}
});
return $self;
}
sub none {
my ($self, $code) = @_;
$self->on_none(UNIVERSAL::isa($code, 'CODE') ? $code : sub{$code});
return $self;
}
sub only {
my ($self, $code) = @_;
$self->on_only($code);
return $self;
}
sub result {
my ($self, @args) = @_;
$self->value($args[0]) if @args;
my $result;
my $matched = 0;
my $value = $self->value;
local $_ = $value;
return wantarray ? ($result, $matched) : $result if !$self->on_only->($value);
for (my $i = 0; $i < @{$self->on_when}; $i++) {
if ($self->on_when->[$i]->($value)) {
$result = $self->on_then->[$i]->($value);
$matched++;
last;
}
}
if (!$matched) {
local $_ = $value;
$result = $self->on_none->($value);
}
return wantarray ? ($result, $matched) : $result;
}
sub test {
my ($self) = @_;
my $matched = 0;
my $value = $self->value;
local $_ = $value;
return $matched if !$self->on_only->($value);
for (my $i = 0; $i < @{$self->on_when}; $i++) {
if ($self->on_when->[$i]->($value)) {
$matched++;
last;
}
}
return $matched;
}
sub then {
my ($self, $code) = @_;
my $next = $#{$self->on_when};
$self->on_then->[$next] = UNIVERSAL::isa($code, 'CODE') ? $code : sub{$code};
return $self;
}
sub when {
my ($self, $code, @args) = @_;
my $next = (@{$self->on_when}-$#{$self->on_then}) > 1 ? -1 : @{$self->on_when};
$self->on_when->[$next] = sub {
(local $_ = $_[0])->$code(@args);
};
return $self;
}
sub where {
my ($self) = @_;
my $where = $self->new;
$self->then(sub{$where->result(@_)});
return $where;
}
1;
=head1 NAME
Venus::Match - Match Class
=cut
=head1 ABSTRACT
Match Class for Perl 5
=cut
=head1 SYNOPSIS
package main;
use Venus::Match;
my $match = Venus::Match->new(5);
$match->when(sub{$_ < 5})->then(sub{"< 5"});
$match->when(sub{$_ > 5})->then(sub{"> 5"});
$match->none(sub{"?"});
my $result = $match->result;
# "?"
=cut
=head1 DESCRIPTION
This package provides an object-oriented interface for complex pattern matching
operations.
=cut
=head1 ATTRIBUTES
This package has the following attributes:
=cut
=head2 on_none
on_none(CodeRef)
This attribute is read-write, accepts C<(CodeRef)> values, is optional, and defaults to C<sub{}>.
=cut
=head2 on_only
on_only(CodeRef)
This attribute is read-write, accepts C<(CodeRef)> values, is optional, and defaults to C<sub{1}>.
=cut
=head2 on_then
on_then(ArrayRef[CodeRef])
This attribute is read-write, accepts C<(ArrayRef[CodeRef])> values, is optional, and defaults to C<[]>.
=cut
=head2 on_when
on_when(ArrayRef[CodeRef])
This attribute is read-write, accepts C<(ArrayRef[CodeRef])> values, is optional, and defaults to C<[]>.
=cut
=head1 INHERITS
This package inherits behaviors from:
L<Venus::Kind::Utility>
=cut
=head1 INTEGRATES
This package integrates behaviors from:
L<Venus::Role::Accessible>
L<Venus::Role::Buildable>
L<Venus::Role::Valuable>
=cut
=head1 METHODS
This package provides the following methods:
=cut
=head2 clear
clear() (Match)
The clear method resets all match conditions and returns the invocant.
I<Since C<1.23>>
=over 4
=item clear example 1
# given: synopsis
package main;
my $clear = $match->clear;
# bless(..., "Venus::Match")
=back
=cut
=head2 data
data(HashRef $data) (Match)
The data method takes a hashref (i.e. lookup table) and match conditions and
actions based on the keys and values found.
I<Since C<0.07>>
=over 4
=item data example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->data({
'a' => 'b',
'c' => 'd',
'e' => 'f',
'g' => 'h',
});
my $result = $match->none('z')->result;
# "b"
=back
=over 4
=item data example 2
package main;
use Venus::Match;
my $match = Venus::Match->new('x');
$match->data({
'a' => 'b',
'c' => 'd',
'e' => 'f',
'g' => 'h',
});
my $result = $match->none('z')->result;
# "z"
=back
=cut
=head2 expr
expr(Str | RegexpRef $expr) (Match)
The expr method registers a L</when> condition that check if the match value is
an exact string match of the C<$topic> if the topic is a string, or that it
matches against the topic if the topic is a regular expression.
I<Since C<0.07>>
=over 4
=item expr example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('1901-01-01');
$match->expr('1901-01-01')->then(sub{[split /-/]});
my $result = $match->result;
# ["1901", "01", "01"]
=back
=over 4
=item expr example 2
package main;
use Venus::Match;
my $match = Venus::Match->new('1901-01-01');
$match->expr(qr/^1901-/)->then(sub{[split /-/]});
my $result = $match->result;
# ["1901", "01", "01"]
=back
=cut
=head2 just
just(Str $topic) (Match)
The just method registers a L</when> condition that check if the match value is
an exact string match of the C<$topic> provided.
I<Since C<0.03>>
=over 4
=item just example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my $result = $match->result;
# "a"
=back
=over 4
=item just example 2
package main;
use Venus::Match;
use Venus::String;
my $match = Venus::Match->new(Venus::String->new('a'));
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my $result = $match->result;
# "a"
=back
=over 4
=item just example 3
package main;
use Venus::Match;
use Venus::String;
my $match = Venus::Match->new(Venus::String->new('c'));
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my $result = $match->result;
# "c"
=back
=over 4
=item just example 4
package main;
use Venus::Match;
my $match = Venus::Match->new(1.23);
$match->just('1.230')->then('1.230');
$match->just(01.23)->then('123');
$match->just(1.230)->then(1.23);
my $result = $match->result;
# "1.23"
=back
=over 4
=item just example 5
package main;
use Venus::Match;
use Venus::Number;
my $match = Venus::Match->new(Venus::Number->new(1.23));
$match->just('1.230')->then('1.230');
$match->just(01.23)->then('123');
$match->just(1.230)->then(1.23);
my $result = $match->result;
# "1.23"
=back
=over 4
=item just example 6
package main;
use Venus::Match;
use Venus::Number;
my $match = Venus::Match->new(1.23);
$match->just(Venus::Number->new('1.230'))->then('1.230');
$match->just(Venus::Number->new(01.23))->then('123');
$match->just(Venus::Number->new(1.230))->then(1.23);
my $result = $match->result;
# "1.23"
=back
=cut
=head2 none
none(Any | CodeRef $code) (Match)
The none method registers a special condition that returns a result only when
no other conditions have been matched.
I<Since C<0.03>>
=over 4
=item none example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('z');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
$match->none('z');
my $result = $match->result;
# "z"
=back
=over 4
=item none example 2
package main;
use Venus::Match;
my $match = Venus::Match->new('z');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
$match->none(sub{"($_) not found"});
my $result = $match->result;
# "(z) not found"
=back
=cut
=head2 only
only(CodeRef $code) (Match)
The only method registers a special condition that only allows matching on the
match value only if the code provided returns truthy.
I<Since C<0.03>>
=over 4
=item only example 1
package main;
use Venus::Match;
my $match = Venus::Match->new(5);
$match->only(sub{$_ != 5});
$match->just(5)->then(5);
$match->just(6)->then(6);
my $result = $match->result;
# undef
=back
=over 4
=item only example 2
package main;
use Venus::Match;
my $match = Venus::Match->new(6);
$match->only(sub{$_ != 5});
$match->just(5)->then(5);
$match->just(6)->then(6);
my $result = $match->result;
# 6
=back
=cut
=head2 result
result(Any $data) (Any)
The result method evaluates the registered conditions and returns the result of
the action (i.e. the L</then> code) or the special L</none> condition if there
were no matches. In list context, this method returns both the result and
whether or not a condition matched. Optionally, when passed an argument this
method assign the argument as the value/topic and then perform the operation.
I<Since C<0.03>>
=over 4
=item result example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my $result = $match->result;
# "a"
=back
=over 4
=item result example 2
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my ($result, $matched) = $match->result;
# ("a", 1)
=back
=over 4
=item result example 3
package main;
use Venus::Match;
sub fibonacci {
my ($n) = @_;
my $match = Venus::Match->new($n)
->just(1)->then(1)
->just(2)->then(1)
->none(sub{fibonacci($n - 1) + fibonacci($n - 2)})
->result
}
my $result = [fibonacci(4), fibonacci(6), fibonacci(12)]
# [3, 8, 144]
=back
=over 4
=item result example 4
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my $result = $match->result('b');
# "b"
=back
=over 4
=item result example 5
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->just('a')->then('a');
$match->just('b')->then('b');
$match->just('c')->then('c');
my $result = $match->result('z');
# undef
=back
=cut
=head2 then
then(Any | CodeRef $code) (Match)
The then method registers an action to be executed if the corresponding match
condition returns truthy.
I<Since C<0.03>>
=over 4
=item then example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('b');
$match->just('a');
$match->then('a');
$match->just('b');
$match->then('b');
my $result = $match->result;
# "b"
=back
=over 4
=item then example 2
package main;
use Venus::Match;
my $match = Venus::Match->new('b');
$match->just('a');
$match->then('a');
$match->just('b');
$match->then('b');
$match->then('x');
my $result = $match->result;
# "x"
=back
=cut
=head2 when
when(Str | CodeRef $code, Any @args) (Match)
The when method registers a match condition that will be passed the match value
during evaluation. If the match condition returns truthy the corresponding
action will be used to return a result. If the match value is an object, this
method can take a method name and arguments which will be used as a match
condition.
I<Since C<0.03>>
=over 4
=item when example 1
package main;
use Venus::Match;
my $match = Venus::Match->new('a');
$match->when(sub{$_ eq 'a'});
$match->then('a');
$match->when(sub{$_ eq 'b'});
$match->then('b');
$match->when(sub{$_ eq 'c'});
$match->then('c');
my $result = $match->result;
# "a"
=back
=over 4
=item when example 2
package main;
use Venus::Match;
use Venus::Type;
my $match = Venus::Match->new(Venus::Type->new(1)->deduce);
$match->when('isa', 'Venus::Number');
$match->then('Venus::Number');
$match->when('isa', 'Venus::String');
$match->then('Venus::String');
my $result = $match->result;
# "Venus::Number"
=back
=over 4
=item when example 3
package main;
use Venus::Match;
use Venus::Type;
my $match = Venus::Match->new(Venus::Type->new('1')->deduce);
$match->when('isa', 'Venus::Number');
$match->then('Venus::Number');
$match->when('isa', 'Venus::String');
$match->then('Venus::String');
my $result = $match->result;
# "Venus::String"
=back
=cut
=head2 where
where() (Match)
The where method registers an action as a sub-match operation, to be executed
if the corresponding match condition returns truthy. This method returns the
sub-match object.
I<Since C<1.40>>
=over 4
=item where example 1
package main;
use Venus::Match;
my $match = Venus::Match->new;
my $submatch1 = $match->expr(qr/^p([a-z]+)ch/)->where;
$submatch1->just('peach')->then('peach-123');
$submatch1->just('patch')->then('patch-456');
$submatch1->just('punch')->then('punch-789');
my $submatch2 = $match->expr(qr/^m([a-z]+)ch/)->where;
$submatch2->just('merch')->then('merch-123');
$submatch2->just('march')->then('march-456');
$submatch2->just('mouch')->then('mouch-789');
my $result = $match->result('peach');
# "peach-123"
=back
=over 4
=item where example 2
package main;
use Venus::Match;
my $match = Venus::Match->new;
my $submatch1 = $match->expr(qr/^p([a-z]+)ch/)->where;
$submatch1->just('peach')->then('peach-123');
$submatch1->just('patch')->then('patch-456');
$submatch1->just('punch')->then('punch-789');
my $submatch2 = $match->expr(qr/^m([a-z]+)ch/)->where;
$submatch2->just('merch')->then('merch-123');
$submatch2->just('march')->then('march-456');
$submatch2->just('mouch')->then('mouch-789');
my $result = $match->result('march');
# "march-456"
=back
=over 4
=item where example 3
package main;
use Venus::Match;
my $match = Venus::Match->new;
my $submatch1 = $match->expr(qr/^p([a-z]+)ch/)->where;
$submatch1->just('peach')->then('peach-123');
$submatch1->just('patch')->then('patch-456');
$submatch1->just('punch')->then('punch-789');
my $submatch2 = $match->expr(qr/^m([a-z]+)ch/)->where;
$submatch2->just('merch')->then('merch-123');
$submatch2->just('march')->then('march-456');
$submatch2->just('mouch')->then('mouch-789');
my $result = $match->result('pirch');
# undef
=back
=cut