File Coverage

blib/lib/App/optex/rpn.pm
Criterion Covered Total %
statement 23 72 31.9
branch 0 32 0.0
condition 0 23 0.0
subroutine 8 16 50.0
pod 1 7 14.2
total 32 150 21.3


line stmt bran cond sub pod time code
1             package App::optex::rpn;
2              
3             our $VERSION = "1.04";
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             rpn - optex module for Reverse Polish Notation calculation
10              
11             =head1 SYNOPSIS
12              
13             optex -Mrpn command ...
14              
15             =head1 DESCRIPTION
16              
17             B is a module for the B command that detects arguments
18             that look like Reverse Polish Notation (RPN) expressions and replaces
19             them with their calculated results.
20              
21             By default, all arguments are processed automatically when the module
22             is loaded.
23              
24             =head1 MODULE OPTIONS
25              
26             Module options can be set via C<-Mrpn::config(...)> or C<--option>
27             before C<-->.
28              
29             =over 4
30              
31             =item B<--auto>, B<--no-auto>
32              
33             Enable or disable automatic processing of all arguments. Default is
34             enabled. Use C<--no-auto> to disable and process only arguments
35             specified by C<--rpn>.
36              
37             =item B<-p> I, B<--pattern> I
38              
39             Specify a pattern to match RPN expressions. The value can be either a
40             preset name (word characters only, prefix match supported) or a
41             custom regex pattern.
42              
43             When C<--pattern> is specified, C<--auto> is ignored.
44              
45             B
46              
47             =over 4
48              
49             =item C
50              
51             Matches C and extracts the content inside parentheses.
52              
53             =item C
54              
55             Matches C<...=> at the end and extracts the expression before C<=>.
56              
57             =back
58              
59             B
60              
61             When the value contains non-word characters, it is treated as a
62             regex pattern. The pattern must contain a capture group C<(...)> that
63             captures the RPN expression. The entire matched portion is replaced
64             with the calculated result.
65              
66             Examples:
67              
68             # Use preset pattern 'rpn' (or -pr for short)
69             optex -Mrpn -pr -- echo '3600*5' '=' rpn(3600,5*)
70             # outputs: 3600*5 = 18000
71              
72             # Use preset pattern 'equal' (or -pe for short)
73             optex -Mrpn -pe -- echo '3600*5' '=' 3600,5*=
74             # outputs: 3600*5 = 18000
75              
76             # Use custom regex pattern
77             optex -Mrpn --pattern 'calc\[(.*)\]' -- echo calc[3600,5*]
78             # outputs: 18000
79              
80             =item B<--quiet>, B<--no-quiet>
81              
82             Suppress Math::RPN warning messages. Default is enabled. Use
83             C<--no-quiet> to see warnings for invalid expressions.
84              
85             =item B<--verbose>
86              
87             Print diagnostic messages.
88              
89             =back
90              
91             =head1 COMMAND OPTIONS
92              
93             These options are available after C<-->.
94              
95             =over 4
96              
97             =item B<--rpn> I
98              
99             Convert a single RPN expression.
100              
101             optex -Mrpn --no-auto -- printf '%s = %d\n' 3600,5* --rpn 3600,5*
102             # outputs: 3600,5* = 18000
103              
104             =back
105              
106             =head1 EXPRESSIONS
107              
108             An RPN expression requires at least two terms separated by commas or
109             colons. A single term like C will not be converted, but
110             C will produce a random number.
111              
112             =head2 OPERATORS
113              
114             The following operators are supported (case-insensitive):
115              
116             =over 4
117              
118             =item Arithmetic
119              
120             C<+> (ADD), C<-> (SUB), C<*> (MUL), C (DIV), C<%> (MOD),
121             C<++> (INCR), C<--> (DECR), C, C
122              
123             =item Trigonometric
124              
125             C, C, C
126              
127             =item Logarithmic
128              
129             C, C
130              
131             =item Numeric
132              
133             C, C
134              
135             =item Bitwise/Logical
136              
137             C<&> (AND), C<|> (OR), C (NOT), C, C<~>
138              
139             =item Comparison
140              
141             C> (LT), C=> (LE), C<=>/C<==> (EQ),
142             C> (GT), C=> (GE), C (NE)
143              
144             =item Conditional
145              
146             C
147              
148             =item Stack
149              
150             C, C, C
151              
152             =item Other
153              
154             C, C, C
155              
156             =back
157              
158             See L for detailed descriptions of these operators.
159              
160             =head1 EXAMPLES
161              
162             Convert 5 hours to seconds (3600 * 5 = 18000):
163              
164             $ optex -Mrpn echo 3600,5*
165             18000
166              
167             Prevent macOS from sleeping for 5 hours:
168              
169             $ optex -Mrpn caffeinate -d -t 3600,5*
170              
171             Process multiple expressions:
172              
173             $ optex -Mrpn echo 1,2+ 10,3*
174             3 30
175              
176             Generate a random number:
177              
178             $ optex -Mrpn echo RAND,0+
179             0.316809834520431
180              
181             =head1 INSTALLATION
182              
183             =head2 CPANMINUS
184              
185             cpanm App::optex::rpn
186              
187             =head1 SEE ALSO
188              
189             L, L
190              
191             L, L
192              
193             L
194              
195             L
196              
197             =head1 AUTHOR
198              
199             Kazumasa Utashiro
200              
201             =head1 LICENSE
202              
203             Copyright 2021-2025 Kazumasa Utashiro.
204              
205             This library is free software; you can redistribute it and/or modify
206             it under the same terms as Perl itself.
207              
208             =cut
209              
210 1     1   383682 use v5.14;
  1         4  
211 1     1   8 use warnings;
  1         2  
  1         95  
212 1     1   9 use Carp;
  1         2  
  1         148  
213 1     1   690 use utf8;
  1         346  
  1         9  
214 1     1   672 use open IO => 'utf8', ':std';
  1         1719  
  1         6  
215 1     1   880 use Data::Dumper;
  1         11663  
  1         101  
216 1     1   704 use Getopt::EX::Config;
  1         30717  
  1         11  
217             my $config = Getopt::EX::Config->new(
218             auto => 1,
219             verbose => 0,
220             quiet => 1,
221             pattern => undef,
222             );
223              
224             my($mod, $argv);
225 0     0 0   sub initialize { ($mod, $argv) = @_ }
226              
227             sub finalize {
228 0     0 0   $config->deal_with($argv, 'auto!', 'verbose!', 'quiet!', 'pattern|p=s');
229 0 0         if (defined $config->{pattern}) {
    0          
230 0           rpn_pattern($config->{pattern});
231             } elsif ($config->{auto}) {
232 0           rpn();
233             }
234             }
235              
236             sub argv (&) {
237 0     0 0   my $sub = shift;
238 0           @$argv = $sub->(@$argv);
239             }
240              
241             my @operator = sort { length $b <=> length $a } split /[,\s]+/, <<'END';
242             +,ADD ++,INCR -,SUB --,DECR *,MUL /,DIV %,MOD POW SQRT
243             SIN COS TAN
244             LOG EXP
245             ABS INT
246             &,AND |,OR !,NOT XOR ~
247             <,LT <=,LE =,==,EQ >,GT >=,GE !=,NE
248             IF
249             DUP EXCH POP
250             MIN MAX
251             TIME
252             RAND LRAND
253             END
254              
255             my $operator_re = join '|', map "\Q$_", @operator;
256             my $term_re = qr/(?:\d*\.)?\d+|$operator_re/i;
257             my $rpn_re = qr/(?: $term_re [,:]* ){2,}/xi;
258              
259             my %preset_pattern = (
260             rpn => qr/^rpn\((.+)\)$/,
261             equal => qr/^(.+)=$/,
262             );
263              
264             sub rpn_calc {
265 1     1   5810 use Math::RPN ();
  1         3514  
  1         2102  
266 0     0 0   my @terms = map { /$term_re/g } @_;
  0            
267 0           my @ans = do {
268 0           local $_;
269 0 0   0     local $SIG{__WARN__} = $config->{quiet} ? sub {} : undef;
270 0           Math::RPN::rpn @terms;
271             };
272 0 0 0       if (@ans == 1 && defined $ans[0] && $ans[0] !~ /[^\.\d]/) {
      0        
273 0           $ans[0];
274             } else {
275 0           return undef;
276             }
277             }
278              
279              
280             sub rpn {
281 0     0 1   my $count = 0;
282 0           for (@$argv) {
283 0 0         /^$rpn_re$/ or next;
284 0   0       my $calc = rpn_calc($_) // next;
285 0 0         if ($calc ne $_) {
286 0           $count++;
287 0           $_ = $calc;
288             }
289             }
290 0 0 0       warn "rpn: converted $count expression(s)\n" if $config->{verbose} && $count;
291             }
292              
293             sub rpn_pattern {
294 0     0 0   my $pattern = shift;
295 0           my $re;
296 0 0         if ($pattern =~ /^\w+$/) {
297 0           my @matches = grep { /^\Q$pattern/ } keys %preset_pattern;
  0            
298 0 0         @matches == 1 or die @matches == 0
    0          
299             ? "rpn: unknown preset pattern '$pattern'\n"
300             : "rpn: ambiguous preset pattern '$pattern' (matches: @matches)\n";
301 0           $re = $preset_pattern{$matches[0]};
302             } else {
303 0           $re = qr/$pattern/;
304             }
305 0           my $count = 0;
306 0           for (@$argv) {
307 0 0         /$re/ or next;
308 0   0       my $expr = $1 // next;
309 0 0         length $expr or next;
310 0   0       my $calc = rpn_calc($expr) // next;
311 0 0         if ($calc ne $expr) {
312 0           $count++;
313 0           s/$re/$calc/;
314             }
315             }
316 0 0 0       warn "rpn: converted $count expression(s)\n" if $config->{verbose} && $count;
317             }
318              
319             sub convert {
320 0     0 0   my %arg = @_;
321 0   0       my $target = $arg{rpn} // die "rpn: missing expression\n";
322 0 0         if ($target =~ /^$rpn_re$/) {
323 0           my $calc = rpn_calc($target);
324 0 0 0       if (defined $calc && $calc ne $target) {
325 0           $target = $calc;
326             }
327             }
328 0           unshift @$argv, $target;
329             }
330              
331             1;
332              
333             __DATA__