2014-06-23 22:47:28 -07:00
|
|
|
package DDG::GoodieRole::NumberStyle;
|
2014-08-20 11:45:33 -07:00
|
|
|
# ABSTRACT: An object representing a particular numerical notation.
|
2014-06-23 22:47:28 -07:00
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
use Moo;
|
|
|
|
|
2014-08-06 11:06:27 -07:00
|
|
|
has [qw(id decimal thousands)] => (
|
2014-06-23 22:47:28 -07:00
|
|
|
is => 'ro',
|
|
|
|
);
|
|
|
|
|
2014-08-06 11:06:27 -07:00
|
|
|
has exponential => (
|
|
|
|
is => 'ro',
|
|
|
|
default => sub { 'e' },
|
|
|
|
);
|
|
|
|
|
2014-07-27 18:24:40 -07:00
|
|
|
has number_regex => (
|
|
|
|
is => 'lazy',
|
|
|
|
);
|
|
|
|
|
|
|
|
sub _build_number_regex {
|
|
|
|
my $self = shift;
|
|
|
|
my ($decimal, $thousands, $exponential) = ($self->decimal, $self->thousands, $self->exponential);
|
|
|
|
|
2014-11-13 04:58:57 -08:00
|
|
|
return qr/-?[\d_ \Q$decimal\E\Q$thousands\E]+(?:\Q$exponential\E-?\d+)?/;
|
2014-07-27 18:24:40 -07:00
|
|
|
}
|
|
|
|
|
2014-06-23 22:47:28 -07:00
|
|
|
sub understands {
|
|
|
|
my ($self, $number) = @_;
|
|
|
|
my ($decimal, $thousands) = ($self->decimal, $self->thousands);
|
|
|
|
|
2014-06-29 05:38:32 -07:00
|
|
|
# How do we know if a number is reasonable for this style?
|
2014-07-27 05:49:32 -07:00
|
|
|
# This assumes the exponentials are not included to give better answers.
|
2014-06-23 22:47:28 -07:00
|
|
|
return (
|
2014-06-29 16:06:24 -07:00
|
|
|
# The number must contain only things we understand: numerals and separators for this style.
|
2014-11-13 04:58:57 -08:00
|
|
|
$number =~ /^-?(|\d|_| |\Q$thousands\E|\Q$decimal\E)+$/
|
2014-06-29 05:38:32 -07:00
|
|
|
&& (
|
2014-06-29 16:06:24 -07:00
|
|
|
# The number is not required to contain thousands separators
|
2014-06-29 05:38:32 -07:00
|
|
|
$number !~ /\Q$thousands\E/
|
|
|
|
|| (
|
|
|
|
# But if the number does contain thousands separators, they must delimit exactly 3 numerals.
|
2014-06-29 16:06:24 -07:00
|
|
|
$number !~ /\Q$thousands\E\d{1,2}\b/
|
|
|
|
&& $number !~ /\Q$thousands\E\d{4,}/
|
|
|
|
# And cannot follow a leading zero
|
2014-06-29 05:38:32 -07:00
|
|
|
&& $number !~ /^0\Q$thousands\E/
|
|
|
|
))
|
|
|
|
&& (
|
2014-06-29 16:06:24 -07:00
|
|
|
# The number is not required to include decimal separators
|
2014-06-29 05:38:32 -07:00
|
|
|
$number !~ /\Q$decimal\E/
|
|
|
|
# But if one is included, it cannot be followed by another separator, whether decimal or thousands.
|
2014-06-29 16:06:24 -07:00
|
|
|
|| $number !~ /\Q$decimal\E(?:.*)?(?:\Q$decimal\E|\Q$thousands\E)/
|
2014-06-29 05:38:32 -07:00
|
|
|
)) ? 1 : 0;
|
2014-06-23 22:47:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
sub precision_of {
|
|
|
|
my ($self, $number_text) = @_;
|
|
|
|
my $decimal = $self->decimal;
|
|
|
|
|
|
|
|
return ($number_text =~ /\Q$decimal\E(\d+)/) ? length($1) : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub for_computation {
|
|
|
|
my ($self, $number_text) = @_;
|
2014-07-27 05:49:32 -07:00
|
|
|
my ($decimal, $thousands, $exponential) = ($self->decimal, $self->thousands, $self->exponential);
|
2014-06-23 22:47:28 -07:00
|
|
|
|
2014-11-13 04:58:57 -08:00
|
|
|
$number_text =~ s/[ _]//g; # Remove spaces and underscores as visuals.
|
2014-06-23 22:47:28 -07:00
|
|
|
$number_text =~ s/\Q$thousands\E//g; # Remove thousands seps, since they are just visual.
|
|
|
|
$number_text =~ s/\Q$decimal\E/./g; # Make sure decimal mark is something perl knows how to use.
|
2014-07-27 19:05:05 -07:00
|
|
|
if ($number_text =~ s/^([\d$decimal$thousands]+)\Q$exponential\E(-?[\d$decimal$thousands]+)$/$1e$2/ig) {
|
2014-07-27 05:49:32 -07:00
|
|
|
# Convert to perl style exponentials and then make into human-style floats.
|
|
|
|
$number_text = sprintf('%f', $number_text);
|
|
|
|
}
|
2014-06-23 22:47:28 -07:00
|
|
|
|
|
|
|
return $number_text;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub for_display {
|
|
|
|
my ($self, $number_text) = @_;
|
2014-07-27 05:49:32 -07:00
|
|
|
my ($decimal, $thousands, $exponential) = ($self->decimal, $self->thousands, $self->exponential);
|
2014-06-23 22:47:28 -07:00
|
|
|
|
2014-11-13 04:58:57 -08:00
|
|
|
$number_text =~ s/[ _]//g; # Remove spaces and underscores as visuals.
|
2014-07-27 18:24:40 -07:00
|
|
|
if ($number_text =~ /(.*)\Q$exponential\E([+-]?\d+)/i) {
|
|
|
|
$number_text = $self->for_display($1) . ' * 10^' . $self->for_display(int $2);
|
2014-07-27 05:49:32 -07:00
|
|
|
} else {
|
|
|
|
$number_text = reverse $number_text;
|
|
|
|
$number_text =~ s/\./$decimal/g; # Perl decimal mark to whatever we need.
|
2014-08-06 13:51:56 -07:00
|
|
|
$number_text =~ s/(\d{3})(?=\d)(?!\d*\Q$decimal\E)/$1$thousands/g;
|
2014-07-27 05:49:32 -07:00
|
|
|
$number_text = reverse $number_text;
|
|
|
|
}
|
2014-06-23 22:47:28 -07:00
|
|
|
|
2014-07-27 05:49:32 -07:00
|
|
|
return $number_text;
|
2014-06-23 22:47:28 -07:00
|
|
|
}
|
|
|
|
|
2014-08-06 11:08:46 -07:00
|
|
|
# The display version with HTML added:
|
|
|
|
# - superscripted exponents
|
2014-07-27 18:24:40 -07:00
|
|
|
sub with_html {
|
|
|
|
my ($self, $number_text) = @_;
|
|
|
|
|
2014-10-15 02:34:41 -07:00
|
|
|
return $self->_add_html_exponents($number_text);
|
2014-07-27 18:24:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
sub _add_html_exponents {
|
|
|
|
|
|
|
|
my ($self, $string) = @_;
|
|
|
|
|
|
|
|
return $string if ($string !~ /\^/ or $string =~ /^\^|\^$/); # Give back the same thing if we won't deal with it properly.
|
|
|
|
|
|
|
|
my @chars = split //, $string;
|
|
|
|
my $number_re = $self->number_regex;
|
|
|
|
my ($start_tag, $end_tag) = ('<sup>', '</sup>');
|
|
|
|
my ($newly_up, $in_exp_number, $in_exp_parens, %power_parens);
|
|
|
|
my ($parens_count, $number_up) = (0, 0);
|
|
|
|
|
|
|
|
# because of associativity and power-to-power, we need to scan nearly the whole thing
|
2014-10-22 23:58:20 -07:00
|
|
|
for my $index (1 .. $#chars) {
|
2014-07-27 18:24:40 -07:00
|
|
|
my $this_char = $chars[$index];
|
2014-07-31 12:36:57 -07:00
|
|
|
if ($this_char =~ $number_re or ($newly_up && $this_char eq '-')) {
|
2014-07-27 18:24:40 -07:00
|
|
|
if ($newly_up) {
|
|
|
|
$in_exp_number = 1;
|
|
|
|
$newly_up = 0;
|
|
|
|
}
|
|
|
|
} elsif ($this_char eq '(') {
|
|
|
|
$parens_count += 1;
|
|
|
|
$in_exp_number = 0;
|
|
|
|
if ($newly_up) {
|
|
|
|
$in_exp_parens += 1;
|
|
|
|
$power_parens{$parens_count} = 1;
|
|
|
|
$newly_up = 0;
|
|
|
|
}
|
|
|
|
} elsif ($this_char eq '^') {
|
|
|
|
$chars[$index - 1] =~ s/$end_tag$//; # Added too soon!
|
|
|
|
$number_up += 1;
|
|
|
|
$newly_up = 1;
|
|
|
|
$chars[$index] = $start_tag; # Replace ^ with the tag.
|
|
|
|
} elsif ($in_exp_number) {
|
|
|
|
$in_exp_number = 0;
|
|
|
|
$number_up -= 1;
|
|
|
|
$chars[$index] = $end_tag . $chars[$index];
|
|
|
|
} elsif ($number_up && !$in_exp_parens) {
|
|
|
|
# Must have ended another term or more
|
2014-10-22 23:58:20 -07:00
|
|
|
$chars[$index] = ($end_tag x $number_up) . $chars[$index];
|
|
|
|
$number_up -= 1;
|
2014-07-27 18:24:40 -07:00
|
|
|
} elsif ($this_char eq ')') {
|
|
|
|
# We just closed a set of parens, see if it closes one of our things
|
|
|
|
if ($in_exp_parens && $power_parens{$parens_count}) {
|
|
|
|
$chars[$index] .= $end_tag;
|
|
|
|
delete $power_parens{$parens_count};
|
|
|
|
$in_exp_parens -= 1;
|
2014-10-22 23:58:20 -07:00
|
|
|
$number_up -= 1;
|
2014-07-27 18:24:40 -07:00
|
|
|
}
|
|
|
|
$parens_count -= 1;
|
|
|
|
}
|
|
|
|
}
|
2014-10-22 23:58:20 -07:00
|
|
|
my $final = join('', @chars);
|
|
|
|
# We may not have added enough closing tags, because we can't "see" the end.
|
|
|
|
my $up_count = () = $final =~ /$start_tag/g;
|
|
|
|
my $down_count = () = $final =~ /$end_tag/g;
|
|
|
|
# We'll assume we're just supposed to append them now
|
|
|
|
$final .= $end_tag x ($up_count - $down_count);
|
|
|
|
|
|
|
|
return $final;
|
2014-07-27 18:24:40 -07:00
|
|
|
}
|
|
|
|
|
2014-08-06 11:06:27 -07:00
|
|
|
1;
|