I have a class Price
that encapsulates an Int
. I also would like it to have constructors for Num
and Str
. I thought I could do this by making Price::new
a multi method with various type constraints, but this isn't the behavior I expected. It looks like Price.new
is skipping the constructor altogether and going straight to BUILD
, bypassing the casting logic.
I know from looking at other Perl 6 code that using multi method new
is acceptable. However, I haven't been able to find an example of a polymorphic constructor with different type constraints. How do I rewrite this code to force it to use the casting logic in the constructor?
lib/Price.pm6
#!/usr/bin/env perl6 -w
use v6;
unit class Price:ver<0.0.1>;
class X::Price::PriceInvalid is Exception {
has $.price;
method message() {
return "Price $!price not valid"
}
}
# Price is stored in cents USD
has $.price;
multi method new(Int $price) {
say "Int constructor";
return self.bless(:$price);
}
multi method new(Num $price) {
say "Num constructor";
return self.new(Int($price * 100));
}
multi method new(Str $price) {
say "String constructor";
$price .= trans(/<-[0..9.]>/ => '');
unless ($price ~~ m/\.\d**2$/) {
die(X::Price::PriceInvalid(:$price));
}
return self.new(Num($price));
}
submethod BUILD(:$!price) { say "Low-level BUILD constructor" }
method toString() {
return sprintf("%.2f", ($!price/100));
}
t/price.t
#!/usr/bin/env perl6 -w
use v6;
use Test;
use-ok 'Price', 'Module loads';
use Price;
# test constructor with Int
my Int $priceInt = 12345;
my $priceIntObj = Price.new(price => $priceInt);
is $priceIntObj.toString(), '123.45',
'Price from Int serializes correctly';
# test constructor with Num
my $priceNum = Num.new(123.45);
my $priceNumObj = Price.new(price => $priceNum);
is $priceNumObj.toString(), '123.45',
'Price from Num serializes correctly';
# test constructor with Num (w/ extra precision)
my $priceNumExtra = 123.4567890;
my $priceNumExtraObj = Price.new(price => $priceNumExtra);
is $priceNumExtraObj.toString(), '123.45',
'Price from Num with extra precision serializes correctly';
# test constructor with Str
my $priceStr = '$123.4567890';
my $priceStrObj = Price.new(price => $priceStr);
is $priceStrObj.toString(), '123.45',
'Price from Str serializes correctly';
# test constructor with invalid Str that doesn't parse
my $priceStrInvalid = 'monkey';
throws-like { my $priceStrInvalidObj = Price.new(price => $priceStrInvalid) }, X::Price::PriceInvalid,
'Invalid string does not parse';
done-testing;
Output of PERL6LIB=lib/ perl6 t/price.t
ok 1 - Module loads
Low-level BUILD constructor
ok 2 - Price from Int serializes correctly
Low-level BUILD constructor
not ok 3 - Price from Num serializes correctly
# Failed test 'Price from Num serializes correctly'
# at t/price.t line 18
# expected: '123.45'
# got: '1.23'
Low-level BUILD constructor
not ok 4 - Price from Num with extra precision serializes correctly
# Failed test 'Price from Num with extra precision serializes correctly'
# at t/price.t line 24
# expected: '123.45'
# got: '1.23'
Low-level BUILD constructor
Cannot convert string to number: base-10 number must begin with valid digits or '.' in '⏏\$123.4567890' (indicated by ⏏)
in method toString at lib/Price.pm6 (Price) line 39
in block <unit> at t/price.t line 30
All of the new
multi methods that you wrote take one positional argument.
:( Int $ )
:( Num $ )
:( Str $ )
You are calling new with a named argument though
:( :price($) )
The problem is that since you didn't write one that would accept that, it uses the default new
that Mu
provides.
If you don't want to allow the built-in new
, you could write a proto
method to prevent it from searching up the inheritance chain.
proto method new (|) {*}
If you want you could also use it to ensure that all potential sub-classes also follow the rule about having exactly one positional parameter.
proto method new ($) {*}
If you want to use named parameters, use them.
multi method new (Int :$price!){…}
You might want to leave new
alone and use multi submethod BUILD
instead.
multi submethod BUILD (Int :$!price!) {
say "Int constructor";
}
multi submethod BUILD (Num :$price!) {
say "Num constructor";
$!price = Int($price * 100);
}
multi submethod BUILD (Str :$price!) {
say "String constructor";
$price .= trans(/<-[0..9.]>/ => '');
unless ($price ~~ m/\.\d**2$/) {
die(X::Price::PriceInvalid(:$price));
}
$!price = Int($price * 100);
}
Actually I would always multiply the input by 100
, so that 1
would be the same as "1"
and 1/1
and 1e0
.
I would also divide the output by 100 to get a Rat.
unit class Price:ver<0.0.1>;
class X::Price::PriceInvalid is Exception {
has $.price;
method message() {
return "Price $!price not valid"
}
}
# Price is stored in cents USD
has Int $.price is required;
method price () {
$!price / 100; # return a Rat
}
# Real is all Numeric values except Complex
multi submethod BUILD ( Real :$price ){
$!price = Int($price * 100);
}
multi submethod BUILD ( Str :$price ){
$price .= trans(/<-[0..9.]>/ => '');
unless ($price ~~ m/\.\d**2$/) {
X::Price::PriceInvalid(:$price).throw;
}
$!price = Int($price * 100);
}
method Str() {
return sprintf("%.2f", ($!price/100));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With