File Coverage

blib/lib/Math/Symbolic/Custom/Factor.pm
Criterion Covered Total %
statement 336 371 90.5
branch 136 178 76.4
condition 79 114 69.3
subroutine 19 19 100.0
pod 0 9 0.0
total 570 691 82.4


line stmt bran cond sub pod time code
1             package Math::Symbolic::Custom::Factor;
2              
3 2     2   280092 use 5.006;
  2         13  
4 2     2   11 use strict;
  2         3  
  2         68  
5 2     2   9 use warnings;
  2         3  
  2         115  
6 2     2   9 no warnings 'recursion';
  2         3  
  2         167  
7              
8             =pod
9              
10             =encoding utf8
11              
12             =head1 NAME
13              
14             Math::Symbolic::Custom::Factor - Re-arrange a Math::Symbolic expression into a product of factors
15              
16             =head1 VERSION
17              
18             Version 0.13
19              
20             =cut
21              
22             our $VERSION = '0.13';
23              
24 2     2   751 use Math::Symbolic qw(:all);
  2         167172  
  2         537  
25 2     2   17 use Math::Symbolic::Custom::Base;
  2         3  
  2         73  
26 2     2   1862 use Math::Symbolic::Custom::Collect 0.32;
  2         33447  
  2         18  
27 2     2   1571 use Math::Symbolic::Custom::Polynomial 0.3;
  2         7208  
  2         12  
28              
29 2     2   248 BEGIN {*import = \&Math::Symbolic::Custom::Base::aggregate_import}
30              
31             our $Aggregate_Export = [qw/to_factored/];
32              
33 2     2   12 use Carp;
  2         3  
  2         11467  
34              
35             =head1 DESCRIPTION
36              
37             Provides method to_factored() through the Math::Symbolic module extension class. This method attempts to factorize a Math::Symbolic expression.
38              
39             This is a very early version and can only factor relatively simple expressions. It has a few factoring strategies in it, and hopefully more will come
40             along in later versions if I have time to implement them.
41              
42             =head1 EXAMPLES
43              
44             use strict;
45             use Math::Symbolic qw/:all/;
46             use Math::Symbolic::Custom::Factor;
47              
48             # to_factored() returns the full expression as a product of factors
49             # and an array ref to the factors themselves (so that multiplying them
50             # together and collecting up should produce the original expression).
51             my ($factored, $factors) = parse_from_string("3*x + 12*y")->to_factored();
52             # $factored and the factors in $factors->[] are Math::Symbolic expressions.
53             print "Full expression: $factored\n"; # Full expression: (x + (4 * y)) * 3
54             print "Factors: '", join(q{', '}, @{$factors}), "'\n\n"; # Factors: '3', 'x + (4 * y)'
55              
56             ($factored, $factors) = parse_from_string("x^2 - 81")->to_factored();
57             print "Full expression: $factored\n"; # Full expression: (9 + x) * (x - 9)
58             print "Factors: '", join(q{', '}, @{$factors}), "'\n\n"; # Factors: 'x - 9', '9 + x'
59              
60             ($factored, $factors) = parse_from_string("6*x^2 + 37*x + 6")->to_factored();
61             print "Full expression: $factored\n"; # Full expression: (6 + x) * (1 + (6 * x))
62             print "Factors: '", join(q{', '}, @{$factors}), "'\n\n"; # Factors: '6 + x', '1 + (6 * x)'
63              
64             ($factored, $factors) = parse_from_string("y^4 - 5*y^3 - 5*y^2 + 23*y + 10")->to_factored();
65             print "Full expression: $factored\n"; # Full expression: ((y - 5) * (((y ^ 2) - 1) - (2 * y))) * (2 + y)
66             print "Factors: '", join(q{', '}, @{$factors}), "'\n\n"; # Factors: '2 + y', 'y - 5', '((y ^ 2) - 1) - (2 * y)'
67              
68             # This one does not factor (using the strategies in this module).
69             # The original expression is returned (albeit re-arranged) and the number of entries in
70             # @{$factors} is 1.
71             ($factored, $factors) = parse_from_string("x^2 + 2*x + 2")->to_factored();
72             print "Full expression: $factored\n"; # Full expression: (2 + (2 * x)) + (x ^ 2)
73             print "Factors: '", join(q{', '}, @{$factors}), "'\n"; # Factors: '(2 + (2 * x)) + (x ^ 2)'
74             print "Did not factorize\n\n" if scalar(@{$factors}) == 1; # Did not factorize
75              
76             =cut
77              
78             sub to_factored {
79 66     66 0 3292089 my ($t1) = @_;
80              
81             # Split the original expression into pre-existing factors.
82             # e.g. if we are given 3*x^2*y then we want (3,x^2,y)
83 66         189 my @original_factors;
84 66         366 get_factors($t1, \@original_factors);
85            
86             # Go through that list of factors and try to factorize more.
87 66         196 my @factors;
88 66         205 FACTORIZE: foreach my $expr (@original_factors) {
89              
90 68         468 my ($t2, $n_hr, $d_hr) = $expr->to_collected();
91            
92 68 50 100     281222 if ( !defined $t2 ) {
    50          
    100          
    100          
    50          
93 0         0 carp "to_factored: undefined result from to_collected() for [$expr]";
94 0         0 return undef;
95             }
96             elsif ( !defined($n_hr) ) {
97             # very simple expression
98 0         0 push @factors, $t2;
99             }
100 68         347 elsif ( scalar(keys %{$n_hr->{terms}}) == 1 ) {
101             # just the constant accumulator
102 1         8 push @factors, $t2;
103             }
104 67         508 elsif ( (scalar(keys %{$n_hr->{terms}}) == 2) && ($n_hr->{terms}{constant_accumulator} == 0)) {
105             # again, just one term
106 2         13 push @factors, $t2;
107             }
108             elsif ( !defined $d_hr ) {
109             # Attempt to factorize this sub-expression using some strategies.
110 65         346 my @new_factors = factorize($t2, $n_hr);
111 65         1172 push @factors, @new_factors;
112             }
113             else {
114             # cannot process at the moment, pass-through
115 0         0 push @factors, $t2;
116             }
117             }
118              
119 66         256 @factors = map { scalar($_->to_collected()) } @factors;
  142         91497  
120 66         158049 my $product_tree = build_product_tree(@factors);
121            
122 66 50       617 return wantarray ? ($product_tree, \@factors) : $product_tree;
123             }
124              
125              
126             sub get_factors {
127 70     70 0 295 my ($t, $l) = @_;
128              
129 70 100 100     349 if ( ($t->term_type() == T_OPERATOR) && ($t->type() == B_PRODUCT) ) {
130 2         35 get_factors($t->op1(), $l);
131 2         12 get_factors($t->op2(), $l);
132             }
133             else {
134 68         1296 push @{$l}, $t;
  68         224  
135             }
136             }
137              
138             sub build_product_tree {
139 66     66 0 269 my @product_list = @_;
140              
141 66         176 my %h;
142              
143 66         170 my @str = map { $_->to_string() } @product_list;
  142         5336  
144 66         8760 $h{$_}++ for @str;
145              
146 66         142 my @to_mult;
147 66         398 foreach my $expr ( sort { $h{$b} <=> $h{$a} } keys %h ) {
  75         356  
148              
149 130         1823608 my $index = $h{$expr};
150              
151 130 100       492 if ( $index > 1 ) {
152 6         46 push @to_mult, parse_from_string("($expr)^$index");
153             }
154             else {
155 124         788 push @to_mult, parse_from_string($expr);
156             }
157             }
158              
159 66         1688871 my $ntp = shift @to_mult;
160 66         327 while (@to_mult) {
161 64         491 my $e = shift @to_mult;
162 64         332 $ntp = Math::Symbolic::Operator->new( '*', $ntp, $e );
163             }
164              
165 66         2075 return $ntp;
166             }
167              
168             sub factorize {
169 65     65 0 194 my ($t, $n_hr) = @_;
170            
171 65         136 my %n_terms = %{ $n_hr->{terms} };
  65         411  
172 65         184 my %n_funcs = %{ $n_hr->{trees} };
  65         245  
173              
174 65         176 my @factors;
175            
176             # extract common constants
177             my %constants;
178 65         217 $constants{$_}++ for grep { $_ != 0 } values %n_terms;
  190         4911  
179 65         238 my @con = sort {$a <=> $b} map { abs } keys %constants;
  125         362  
  162         618  
180 65         235 my @con_int = grep { $_ eq int($_) } @con;
  162         583  
181            
182 65 50       302 if ( scalar(@con) == scalar(@con_int) ) { # all integer, proceed
183 65         149 my $min = $con[0];
184 65         124 my $GCF;
185 65         274 FIND_CONST_GCF: foreach my $div (reverse(2..$min)) {
186 77         136 my $div_ok = 1;
187 77         139 CONST_DIV_TEST: foreach my $num (@con) {
188 127 100       373 if ( $num % $div != 0 ) {
189 64         94 $div_ok = 0;
190 64         139 last CONST_DIV_TEST;
191             }
192             }
193 77 100       203 if ( $div_ok ) {
194 13         25 $GCF = $div;
195 13         41 last FIND_CONST_GCF;
196             }
197             }
198            
199 65 100       257 if ( defined $GCF ) {
200 13         85 push @factors, Math::Symbolic::Constant->new($GCF);
201 13         345 $n_terms{$_} /= $GCF for keys %n_terms;
202             }
203             }
204            
205             # extract common variables
206 65 100       334 if ($n_terms{constant_accumulator} == 0) {
207            
208 19         43 my %c_vars;
209 19         77 foreach my $key (keys %n_terms) {
210 57         182 my @v1 = split(/,/, $key);
211 57         120 foreach my $v2 (@v1) {
212 61         182 my ($v, $p) = split(/:/, $v2);
213 61 100       175 if ( exists $c_vars{$v} ) {
214 5         14 $c_vars{$v}{c}++;
215 5 100       31 if ($p < $c_vars{$v}{p}) {
216 4         16 $c_vars{$v}{p} = $p;
217             }
218             }
219             else {
220 56         166 $c_vars{$v}{c}++;
221 56         185 $c_vars{$v}{p} = $p;
222             }
223             }
224             }
225            
226 19         46 my @all_terms;
227 19         108 while ( my ($v, $c) = each %c_vars ) {
228 56 100       266 if ( $c->{c} == scalar(keys %n_terms)-1 ) {
229 5         28 push @all_terms, [$v, $c->{p}];
230             }
231             }
232              
233 19         81 foreach my $common_var (@all_terms) {
234 5         13 my ($var, $pow) = @{$common_var};
  5         17  
235            
236 5         11 my %new_ct;
237 5         16 $new_ct{constant_accumulator} = 0;
238            
239             # remove this variable from the data structure
240 5         25 while ( my ($t, $c) = each %n_terms ) {
241              
242 15 100       53 next if $t eq 'constant_accumulator';
243 10         28 my @v1 = split(/,/, $t);
244 10         20 my @nt;
245 10         23 foreach my $v2 (@v1) {
246 14         41 my ($vv, $cc) = split(/:/, $v2);
247 14 100       40 if ($vv eq $var) {
248 10         21 $cc -= $pow;
249 10 100       38 if ($cc > 0) {
    100          
250 4         16 push @nt, "$vv:$cc";
251             }
252             elsif ( scalar(@v1) == 1 ) {
253 3         10 $new_ct{constant_accumulator} += $c;
254             }
255             }
256             else {
257 4         11 push @nt, $v2;
258             }
259             }
260              
261 10 100       35 if ( scalar(@nt) ) {
262 7         44 $new_ct{join(",", @nt)} = $c;
263             }
264             }
265            
266             # add it to the list of factors
267 5 50       20 if ( $pow == 1 ) {
268 5         34 push @factors, Math::Symbolic::Variable->new($n_funcs{$var}->new());
269             }
270             else {
271 0         0 push @factors, Math::Symbolic::Operator->new('^', $n_funcs{$var}->new(), Math::Symbolic::Constant->new($pow));
272             }
273            
274 5         274 %n_terms = %new_ct;
275             }
276             }
277            
278             # Various factoring formulas
279             # Difference of Squares:-
280             # x^2-y^2 = (x-y)*(x+y)
281             # Sum/Difference of Cubes:-
282             # x^3-y^3 = (x-y)*(x^2+x*y+y^2)
283             # x^3+y^3 = (x+y)*(x^2−x*y+y^2)
284             # pull out the constant accumulator
285 65         177 my $acc = $n_terms{constant_accumulator};
286 65         179 delete $n_terms{constant_accumulator};
287              
288 65 100 66     646 if ( (scalar(keys %n_terms) == 1) &&
    100 66        
      100        
289             (abs($acc) > 1) &&
290             ($acc eq int($acc)) ) {
291            
292             # remaining key must be the other term
293 24         87 my $t = (keys %n_terms)[0];
294 24         72 my $mult = $n_terms{$t};
295            
296 24 100       92 if ( $mult > 0 ) {
297            
298 22         102 my $cbrt_y = int_rt(abs($acc), 3); # y (accumulator) must be a cube
299 22         88 my $cbrt_x = int_rt($mult, 3); # x constant multiplier must be a cube
300            
301 22 100 100     223 if ( ($t !~ /,/) && ($t =~ /:3$/) && defined($cbrt_x) && defined($cbrt_y) ) {
      100        
      100        
302            
303 1         6 my ($vv) = split(/:/, $t);
304 1         5 my $v = $n_funcs{$vv}->{name};
305            
306 1 50       5 if ( $acc < -1 ) {
    0          
307            
308 1 50       4 if ( $cbrt_x == 1 ) {
309 1         9 push @factors, parse_from_string("$v - $cbrt_y");
310 1         6834 push @factors, parse_from_string("$v^2 + $cbrt_y*$v + " . ($cbrt_y*$cbrt_y));
311             }
312             else {
313 0         0 push @factors, parse_from_string("$cbrt_x*$v - $cbrt_y");
314 0         0 push @factors, parse_from_string(($cbrt_x*$cbrt_x) . "*$v^2 + " . ($cbrt_y*$cbrt_x) . "*$v + " . ($cbrt_y*$cbrt_y));
315             }
316             }
317             elsif ( $acc > 1 ) {
318            
319 0 0       0 if ( $cbrt_x == 1 ) {
320 0         0 push @factors, parse_from_string("$v + $cbrt_y");
321 0         0 push @factors, parse_from_string("$v^2 - $cbrt_y*$v + " . ($cbrt_y*$cbrt_y));
322             }
323             else {
324 0         0 push @factors, parse_from_string("$cbrt_x*$v + $cbrt_y");
325 0         0 push @factors, parse_from_string(($cbrt_x*$cbrt_x) . "*$v^2 - " . ($cbrt_y*$cbrt_x) . "*$v + " . ($cbrt_y*$cbrt_y));
326             }
327             }
328              
329 1         10880 undef %n_terms;
330 1         11 return @factors;
331             }
332            
333 21 100 66     147 if ( %n_terms && ($acc < -1) ) {
334            
335 20         77 my $sqrt_y = int_rt(abs($acc), 2); # y (accumulator) must be a square
336 20         54 my $sqrt_x = int_rt($mult, 2); # x constant multiplier must be a square
337 20         110 my ($vv, $pow) = split(/:/, $t); # power must be a square (> 1)
338              
339 20 100 100     254 if ( ($t !~ /,/) && defined($sqrt_x) && ($pow > 1) && ($pow % 2 == 0) && defined($sqrt_y) ) {
      100        
      100        
      100        
340            
341 8         29 my $v = $n_funcs{$vv}->{name};
342            
343 8 100       31 if ( $pow == 2 ) {
    50          
344 5 100       14 if ( $sqrt_x == 1 ) {
345 4         27 push @factors, parse_from_string("$v - $sqrt_y");
346 4         23673 push @factors, parse_from_string("$v + $sqrt_y");
347             }
348             else {
349 1         7 push @factors, parse_from_string("($sqrt_x*$v) - $sqrt_y");
350 1         10069 push @factors, parse_from_string("($sqrt_x*$v) + $sqrt_y");
351             }
352             }
353             elsif ( $pow % 2 == 0 ) {
354 3         10 my $hp = $pow / 2;
355 3 100       10 if ( $sqrt_x == 1 ) {
356 2         32 push @factors, parse_from_string("$v^$hp - $sqrt_y");
357 2         11780 push @factors, parse_from_string("$v^$hp + $sqrt_y");
358             }
359             else {
360 1         17 push @factors, parse_from_string("($sqrt_x*$v^$hp) - $sqrt_y");
361 1         17813 push @factors, parse_from_string("($sqrt_x*$v^$hp) + $sqrt_y");
362             }
363             }
364            
365 8         62368 undef %n_terms;
366 8         77 return @factors;
367             }
368             }
369             }
370             }
371             elsif ( (scalar(keys %n_terms) == 2) && ($acc == 0) ) {
372            
373             # try the factoring formulas with two variables
374 16         68 my @t = keys %n_terms;
375 16         49 my @m = map { $n_terms{$_} } @t;
  32         94  
376              
377 16         87 my ($vv1) = split(/:/, $t[0]);
378 16         56 my $v1 = $n_funcs{$vv1}->{name};
379 16         54 my ($vv2) = split(/:/, $t[1]);
380 16         53 my $v2 = $n_funcs{$vv2}->{name};
381              
382             # x^2-y^2 = (x-y)*(x+y)
383 16 100 66     403 if ( ($t[0] !~ /,/) && ($t[0] =~ /:2$/) && ($t[1] !~ /,/) && ($t[1] =~ /:2$/) ) {
    100 66        
      66        
      66        
      66        
      66        
384            
385 1 50 33     13 if ( ($m[0] > 0) && ($m[1] < 0) ) {
    50 33        
386 0         0 my $sqrt_y = int_rt(abs($m[1]), 2);
387 0         0 my $sqrt_x = int_rt($m[0], 2);
388              
389 0 0 0     0 if ( defined($sqrt_y) && defined($sqrt_x) ) {
390 0 0 0     0 if ( ($sqrt_x == 1) && ($sqrt_y == 1) ) {
    0          
    0          
391 0         0 push @factors, parse_from_string("$v1 - $v2");
392 0         0 push @factors, parse_from_string("$v1 + $v2");
393             }
394             elsif ( $sqrt_x == 1 ) {
395 0         0 push @factors, parse_from_string("$v1 - $sqrt_y*$v2");
396 0         0 push @factors, parse_from_string("$v1 + $sqrt_y*$v2");
397             }
398             elsif ( $sqrt_y == 1 ) {
399 0         0 push @factors, parse_from_string("$sqrt_x*$v1 - $v2");
400 0         0 push @factors, parse_from_string("$sqrt_x*$v1 + $v2");
401             }
402             else {
403 0         0 push @factors, parse_from_string("$sqrt_x*$v1 - $sqrt_y*$v2");
404 0         0 push @factors, parse_from_string("$sqrt_x*$v1 + $sqrt_y*$v2");
405             }
406              
407 0         0 undef %n_terms;
408 0         0 return @factors;
409             }
410             }
411             elsif ( ($m[0] < 0) && ($m[1] > 0 ) ) {
412 1         6 my $sqrt_y = int_rt($m[1], 2);
413 1         5 my $sqrt_x = int_rt(abs($m[0]), 2);
414              
415 1 50 33     8 if ( defined($sqrt_y) && defined($sqrt_x) ) {
416 1 50 33     7 if ( ($sqrt_x == 1) && ($sqrt_y == 1) ) {
    50          
    0          
417 0         0 push @factors, parse_from_string("$v2 - $v1");
418 0         0 push @factors, parse_from_string("$v2 + $v1");
419             }
420             elsif ( $sqrt_y == 1 ) {
421 1         9 push @factors, parse_from_string("$v2 - $sqrt_x*$v1");
422 1         10033 push @factors, parse_from_string("$v2 + $sqrt_x*$v1");
423             }
424             elsif ( $sqrt_x == 1 ) {
425 0         0 push @factors, parse_from_string("$sqrt_y*$v2 - $v1");
426 0         0 push @factors, parse_from_string("$sqrt_y*$v2 + $v1");
427             }
428             else {
429 0         0 push @factors, parse_from_string("$sqrt_y*$v2 - $sqrt_x*$v1");
430 0         0 push @factors, parse_from_string("$sqrt_y*$v2 + $sqrt_x*$v1");
431             }
432              
433 1         9824 undef %n_terms;
434 1         13 return @factors;
435             }
436             }
437             }
438             elsif ( ($t[0] !~ /,/) && ($t[0] =~ /:3$/) && ($t[1] !~ /,/) && ($t[1] =~ /:3$/) ) {
439            
440 12 100 100     119 if ( ($m[0] > 0) && ($m[1] > 0) ) {
    100 66        
    50 33        
441             # x^3+y^3 = (x+y)*(x^2−x*y+y^2)
442            
443 4         19 my $cbrt_x = int_rt($m[0], 3);
444 4         12 my $cbrt_y = int_rt($m[1], 3);
445            
446 4 50 33     26 if ( defined($cbrt_y) && defined($cbrt_x) ) {
447 4 100 100     30 if ( ($cbrt_x == 1) && ($cbrt_y == 1) ) {
    100          
    100          
448 1         10 push @factors, parse_from_string("$v1 + $v2");
449 1         9271 push @factors, parse_from_string("$v1^2 - $v1*$v2 + $v2^2");
450             }
451             elsif ( $cbrt_y == 1 ) {
452 1         9 push @factors, parse_from_string("$cbrt_x*$v1 + $v2");
453 1         10173 push @factors, parse_from_string(($cbrt_x*$cbrt_x) . "*$v1^2 - $cbrt_x*$v1*$v2 + $v2^2");
454             }
455             elsif ( $cbrt_x == 1 ) {
456 1         29 push @factors, parse_from_string("$v1 + $cbrt_y*$v2");
457 1         11045 push @factors, parse_from_string("$v1^2 - $cbrt_y*$v1*$v2 + " . ($cbrt_y*$cbrt_y) . "*$v2^2");
458             }
459             else {
460 1         7 push @factors, parse_from_string("$cbrt_x*$v1 + $cbrt_y*$v2");
461 1         9057 push @factors, parse_from_string(($cbrt_x*$cbrt_x) . "*$v1^2 - " . ($cbrt_x*$cbrt_y) . "*$v1*$v2 + " . ($cbrt_y*$cbrt_y) . "*$v2^2");
462             }
463              
464 4         75388 undef %n_terms;
465 4         56 return @factors;
466             }
467             }
468             elsif ( ($m[0] > 0) && ($m[1] < 0) ) {
469             # x^3-y^3 = (x-y)*(x^2+x*y+y^2)
470            
471 3         17 my $cbrt_x = int_rt($m[0], 3);
472 3         9 my $cbrt_y = int_rt(abs($m[1]), 3);
473              
474 3 50 33     18 if ( defined($cbrt_y) && defined($cbrt_x) ) {
475 3 100 100     36 if ( ($cbrt_x == 1) && ($cbrt_y == 1) ) {
    100          
    50          
476 1         9 push @factors, parse_from_string("$v1 - $v2");
477 1         9258 push @factors, parse_from_string("$v1^2 + $v1*$v2 + $v2^2");
478             }
479             elsif ( $cbrt_y == 1 ) {
480 1         6 push @factors, parse_from_string("$cbrt_x*$v1 - $v2");
481 1         5532 push @factors, parse_from_string(($cbrt_x*$cbrt_x) . "*$v1^2 + $cbrt_x*$v1*$v2 + $v2^2");
482             }
483             elsif ( $cbrt_x == 1 ) {
484 1         8 push @factors, parse_from_string("$v1 - $cbrt_y*$v2");
485 1         10340 push @factors, parse_from_string("$v1^2 + $cbrt_y*$v1*$v2 + " . ($cbrt_y*$cbrt_y) . "*$v2^2");
486             }
487             else {
488 0         0 push @factors, parse_from_string("$cbrt_x*$v1 - $cbrt_y*$v2");
489 0         0 push @factors, parse_from_string(($cbrt_x*$cbrt_x) . "*$v1^2 + " . ($cbrt_x*$cbrt_y) . "*$v1*$v2 + " . ($cbrt_y*$cbrt_y) . "*$v2^2");
490             }
491              
492 3         43940 undef %n_terms;
493 3         36 return @factors;
494             }
495             }
496             elsif ( ($m[0] < 0) && ($m[1] > 0) ) {
497             # y^3-x^3 = (y-x)*(y^2+y*x+x^2)
498              
499 5         25 my $cbrt_x = int_rt(abs($m[0]), 3);
500 5         22 my $cbrt_y = int_rt($m[1], 3);
501              
502 5 50 33     31 if ( defined($cbrt_y) && defined($cbrt_x) ) {
503 5 100 100     37 if ( ($cbrt_x == 1) && ($cbrt_y == 1) ) {
    100          
    100          
504 1         11 push @factors, parse_from_string("$v2 - $v1");
505 1         9801 push @factors, parse_from_string("$v2^2 + $v2*$v1 + $v1^2");
506             }
507             elsif ( $cbrt_y == 1 ) {
508 1         9 push @factors, parse_from_string("$v2 - $cbrt_x*$v1");
509 1         10052 push @factors, parse_from_string("$v2^2 + $cbrt_x*$v2*$v1 + " . ($cbrt_x*$cbrt_x) . "*$v1^2");
510             }
511             elsif ( $cbrt_x == 1 ) {
512 1         8 push @factors, parse_from_string("$cbrt_y*$v2 - $v1");
513 1         9974 push @factors, parse_from_string(($cbrt_y*$cbrt_y) . "*$v2^2 + $cbrt_y*$v2*$v1 + $v1^2");
514             }
515             else {
516 2         18 push @factors, parse_from_string("$cbrt_y*$v2 - $cbrt_x*$v1");
517 2         22800 push @factors, parse_from_string(($cbrt_y*$cbrt_y) . "*$v2^2 + " . ($cbrt_x*$cbrt_y) . "*$v2*$v1 + " . ($cbrt_x*$cbrt_x) . "*$v1^2");
518             }
519              
520 5         87542 undef %n_terms;
521 5         87 return @factors;
522             }
523             }
524             }
525             }
526            
527 43 50       137 if ( %n_terms ) {
528             # rebuild expression and continue attempting to factor
529 43 50       260 if ( !exists $n_terms{constant_accumulator} ) {
530             # put the accumulator back in if necessary
531 43         290 $n_terms{constant_accumulator} = $acc;
532             }
533 43         310 my $nt = Math::Symbolic::Custom::Collect::build_summation_tree({ terms => \%n_terms, trees => \%n_funcs });
534              
535 43         18232 my $factors_in = scalar(@factors);
536              
537 43         161 POLY_FACTOR: while ( defined $nt ) {
538              
539             # test for polynomial
540             # FIXME: looping around test_polynomial() like this is slow
541 65         549 my ($var, $coeffs, $disc, $roots) = $nt->test_polynomial();
542              
543 65 50       3416563 last POLY_FACTOR unless defined $var;
544 65 50       293 last POLY_FACTOR unless defined $coeffs;
545              
546 65         161 my $degree = scalar(@{$coeffs})-1;
  65         222  
547 65 100       336 last POLY_FACTOR if $degree <= 1;
548              
549             # check if all the coefficients are constant integers
550 41         109 my @co = @{$coeffs};
  41         115  
551 148         653 my @const_co = grep { $_ eq int($_) }
552 148         916 map { $_->value() }
553 41         121 grep { ref($_) eq 'Math::Symbolic::Constant'} @co;
  149         421  
554              
555 41 100       188 if ( scalar(@const_co) == scalar(@co) ) {
556            
557             # see if it trivially collapses to a binomial
558 40 50       155 if ( $degree < 15 ) { # FIXME: Upper limit on degree for binomials??
559            
560 40         127 my $const_term = $const_co[-1];
561 40         209 my $root = int_rt(abs($const_term), $degree);
562            
563 40 100       207 if ( defined $root ) {
564            
565 14         43 my @pos_matches;
566             my @neg_matches;
567 14         54 CHECK_BINOMIAL: foreach my $k (0..$degree) {
568              
569 49         200 my $bin_coeff = binomial_coefficient($degree, $k);
570            
571 49         114 my $bin_add = $bin_coeff * $root**$k;
572 49         116 my $bin_sub = $bin_add;
573 49 100       136 $bin_sub *= -1 if $k % 2 == 1;
574            
575 49 100       168 if ( $const_co[$k] == $bin_add ) {
576 25         73 push @pos_matches, $const_co[$k];
577             }
578 49 100       178 if ( $const_co[$k] == $bin_sub ) {
579 24         75 push @neg_matches, $const_co[$k];
580             }
581             }
582            
583 14 100       83 if ( scalar(@pos_matches) == scalar(@const_co) ) {
    100          
584 4         42 push @factors, parse_from_string("$var + $root") for (1..$degree);
585 4         75656 undef %n_terms;
586 4         84 undef $nt;
587 4         58 last POLY_FACTOR;
588             }
589             elsif ( scalar(@neg_matches) == scalar(@const_co) ) {
590 2         22 push @factors, parse_from_string("$var - $root") for (1..$degree);
591 2         51612 undef %n_terms;
592 2         47 undef $nt;
593 2         25 last POLY_FACTOR;
594             }
595             }
596             }
597            
598             # try rational root theorem
599 34         176 my @potential_roots = get_rational_roots($const_co[0], $const_co[-1]);
600 34         82 my $found_root = 0;
601 34         88 TRY_RAT_ROOT: foreach my $root (@potential_roots) {
602              
603 214         1261 my $MS_root = parse_from_string($root);
604 214         726149 my $p_val = $nt->value($var => $MS_root->value());
605              
606 214 100       274347 if ( abs($p_val) < 1e-11 ) { # fix failing test on uselongdouble systems.
607              
608 22         243 $MS_root = $MS_root->to_collected();
609 22         16746 my ($full_expr, $divisor, $quotient, $remainder) = $nt->apply_synthetic_division($MS_root, $var);
610              
611 22 50       2238316 if ( $remainder->value() == 0 ) {
612 22         236 $found_root = 1;
613 22         57 push @factors, $divisor;
614 22         99 $nt = $quotient;
615             }
616              
617 22         317 last TRY_RAT_ROOT;
618             }
619             }
620              
621 34 100       475 unless ( $found_root ) {
622 12         256 last POLY_FACTOR;
623             }
624            
625             }
626             else {
627 1         27 last POLY_FACTOR;
628             }
629              
630             } # /POLY_FACTOR
631              
632 43 100 100     388 if ( ($factors_in < scalar(@factors)) && defined($nt) ) {
633              
634 19         145 my ($t3, $n_hr, $d_hr) = $nt->to_collected();
635 19         48389 $nt = $t3;
636 19         92 %n_terms = %{ $n_hr->{terms} };
  19         123  
637 19         44 %n_funcs = %{ $n_hr->{trees} };
  19         156  
638             }
639              
640             }
641              
642 43 100 66     445 if ( scalar(@factors) == 0 ) {
    100          
643             # could not get any new factors, return the original expression
644 7         65 return $t;
645             }
646             elsif ( %n_terms && (scalar(@factors) > 0) ) {
647             # got some factors and a leftover expression, rebuild and return that too
648 30 50       123 if ( !exists $n_terms{constant_accumulator} ) {
649 0         0 $n_terms{constant_accumulator} = $acc;
650             }
651 30         187 my $nt = Math::Symbolic::Custom::Collect::build_summation_tree({ terms => \%n_terms, trees => \%n_funcs });
652 30         7157 push @factors, $nt;
653             }
654            
655 36         350 return @factors;
656             }
657              
658             sub factor {
659 68     68 0 161 my ($n) = @_;
660              
661 68         132 $n = abs($n);
662 68         121 my %factors;
663 68         217 for my $i (1 .. int(sqrt($n))) {
664 132 100       366 if ($n % $i == 0) {
665 105         247 my $complement = $n / $i;
666 105         318 $factors{$i}++;
667 105         531 $factors{$complement}++;
668             }
669             }
670 68         232 my @factors = keys %factors;
671              
672 68         263 return @factors;
673             }
674              
675             sub get_rational_roots {
676 34     34 0 163 my ($leading, $const_term) = @_;
677            
678 34         140 my @p_factors = factor($const_term);
679 34         92 my @q_factors = factor($leading);
680            
681             # Generate all combinations of p/q
682 34         69 my %roots;
683 34         85 for my $p (@p_factors) {
684 121         215 for my $q (@q_factors) {
685             # Add both positive and negative roots
686 198         489 $roots{"$p / $q"}++;
687 198         542 $roots{"-$p / $q"}++;
688             }
689             }
690            
691 34         247 return keys %roots;
692             }
693              
694             sub int_rt {
695             # determine if there is an integer nth root of a number
696 150     150 0 380 my ($v, $n) = @_;
697 150         586 my $guess = $v**(1/$n);
698 150         299 $guess = int($guess);
699 150 100       606 return $guess if $guess**$n == $v;
700 62         202 $guess += 1;
701 62 50       198 return $guess if $guess**$n == $v;
702 62         194 return undef;
703             }
704              
705             sub factorial {
706 63     63 0 125 my ($num) = @_;
707 63 100       180 return 1 if $num <= 1; # 0! = 1! = 1
708 35         62 my $result = 1;
709 35         134 $result *= $_ for (2..$num);
710 35         79 return $result;
711             }
712              
713             sub binomial_coefficient {
714 49     49 0 120 my ($n, $k) = @_;
715            
716             # Handle edge cases
717 49 50 33     243 return 0 if $k < 0 || $k > $n; # C(n, k) = 0 if k < 0 or k > n
718 49 100 100     206 return 1 if $k == 0 || $k == $n; # C(n, 0) = C(n, n) = 1
719              
720             # C(n, k) = n! / (k! * (n-k)!)
721 21         71 my $numerator = factorial($n);
722 21         52 my $denominator = factorial($k) * factorial($n - $k);
723 21         61 return $numerator / $denominator;
724             }
725              
726             =head1 SEE ALSO
727              
728             L
729              
730             L
731              
732             L
733              
734             =head1 AUTHOR
735              
736             Matt Johnson, C<< >>
737              
738             =head1 ACKNOWLEDGEMENTS
739              
740             Steffen Mueller, author of Math::Symbolic
741              
742             =head1 LICENSE AND COPYRIGHT
743              
744             This software is copyright (c) 2025 by Matt Johnson.
745              
746             This is free software; you can redistribute it and/or modify it under
747             the same terms as the Perl 5 programming language system itself.
748              
749             =cut
750              
751             1;
752             __END__