File Coverage

blib/lib/CXC/Number/Sequence/Linear.pm
Criterion Covered Total %
statement 32 32 100.0
branch n/a
condition n/a
subroutine 11 11 100.0
pod n/a
total 43 43 100.0


line stmt bran cond sub pod time code
1             package CXC::Number::Sequence::Linear;
2              
3             # ABSTRACT: Numeric Sequence with Equal Spacing
4              
5 3     3   775129 use v5.28;
  3         11  
6 3     3   18 use strict;
  3         8  
  3         90  
7 3     3   14 use warnings;
  3         8  
  3         235  
8              
9             use Hash::Wrap 0.11 {
10 3         24 -as => 'wrap_attrs_ro',
11             -immutable => 1,
12             -exists => 'has',
13 3     3   1855 };
  3         15777  
14              
15 3     3   19275 use Types::Standard qw( Optional Bool );
  3         431402  
  3         68  
16 3     3   12801 use CXC::Number::Sequence::Failure -all;
  3         28  
  3         54  
17 3     3   2249 use CXC::Number::Sequence::Types -all;
  3         198  
  3         55  
18 3     3   27189 use CXC::Number::Sequence::Utils qw( buildargs_factory );
  3         13  
  3         43  
19              
20 3     3   2659 use enum qw( BITMASK: MIN MAX SOFT_MIN SOFT_MAX CENTER NELEM SPACING RANGEW ALIGN FORCE_EXTREMA );
  3         4712  
  3         22  
21              
22              
23 3     3   5838 use Moo;
  3         24883  
  3         18  
24              
25             our $VERSION = '0.13';
26              
27             extends 'CXC::Number::Sequence';
28              
29             sub BigFloat { Math::BigFloat->new( @_ ) }
30              
31 3     3   6567 use namespace::clean;
  3         8  
  3         27  
32              
33             has _force_extrema => (
34             is => 'ro',
35             isa => Bool,
36             init_arg => 'force_extrema',
37             default => 0,
38             );
39              
40             my %ArgMap = (
41             align => { type => Optional [Alignment], flag => ALIGN },
42             spacing => { type => Optional [BigPositiveNum], flag => SPACING },
43             center => { type => Optional [BigNum], flag => CENTER },
44             max => { type => Optional [BigNum], flag => MAX },
45             min => { type => Optional [BigNum], flag => MIN },
46             nelem => { type => Optional [BigPositiveInt], flag => NELEM },
47             rangew => { type => Optional [BigPositiveNum], flag => RANGEW },
48             soft_max => { type => Optional [BigNum], flag => SOFT_MAX },
49             soft_min => { type => Optional [BigNum], flag => SOFT_MIN },
50             force_extrema => { type => Optional [Bool], flag => FORCE_EXTREMA },
51             );
52              
53             my sub build_sequence {
54             my $attr = wrap_attrs_ro( shift );
55             my @seq = map { $attr->min + $attr->spacing * $_ } 0 .. ( $attr->nelem - 1 );
56              
57             # make sure that the bounds exactly match what is specified if the
58             # sequence is exactly covering [min,max], just in case roundoff error
59             # occurs.
60             if ( $attr->args->has( 'force_extrema' )
61             && $attr->args->force_extrema )
62             {
63             $seq[0] = $attr->min;
64             $seq[-1] = $attr->max;
65             }
66              
67             return \@seq;
68             }
69              
70             # need the parens otherwise the => operator turns them into strings
71             ## no critic( ControlStructures::ProhibitNegativeExpressionsInUnlessAndUntilConditions )
72             my @ArgsCrossValidate = ( [
73             MIN | MAX,
74             sub {
75             parameter_constraint->throw( 'min < max' )
76             unless $_->min < $_->max;
77             },
78             ],
79              
80             [
81             MIN | SOFT_MAX,
82             sub {
83             parameter_constraint->throw( 'min < soft_max' )
84             unless $_->min < $_->soft_max;
85             },
86             ],
87              
88             [
89             SOFT_MIN | MAX,
90             sub {
91             parameter_constraint->throw( 'soft_min < max' )
92             unless $_->soft_min < $_->max;
93             },
94             ],
95              
96             [
97             SOFT_MIN | SOFT_MAX,
98             sub {
99             parameter_constraint->throw( 'soft_min < soft_max' )
100             unless $_->soft_min < $_->soft_max;
101             },
102             ],
103              
104             [
105             CENTER | SOFT_MIN,
106             sub {
107             parameter_constraint->throw( 'soft_min < center' )
108             unless $_->soft_min < $_->center;
109             },
110             ],
111              
112             [
113             CENTER | SOFT_MAX,
114             sub {
115             parameter_constraint->throw( 'center < soft_max' )
116             unless $_->center < $_->soft_max;
117             },
118             ],
119              
120             [
121             CENTER | MIN,
122             sub {
123             parameter_constraint->throw( 'min < center' )
124             unless $_->min < $_->center;
125             },
126             ],
127              
128             [
129             CENTER | MAX,
130             sub {
131             parameter_constraint->throw( 'center < max' )
132             unless $_->center < $_->max;
133             },
134             ],
135             );
136              
137              
138             my %ArgBuild;
139             %ArgBuild = (
140              
141             #----------------------------------------
142             # The sequence exactly covers [ min, max ]
143             # spacing = ( max - min ) / ( nelem - 1);
144             ( MIN | MAX | NELEM ),
145             sub {
146             {
147             elements => build_sequence( {
148             args => $_,
149             min => $_->min,
150             max => $_->max,
151             nelem => $_->nelem,
152             spacing => ( $_->max - $_->min ) / ( $_->nelem - 1 ),
153             } ) };
154             },
155              
156             #----------------------------------------
157              
158             # The sequence covers [ MIN= (max-min) / 2 - (nelem - 1 ) * spacing, MIN + ( nelem - 1 ) * spacing ]
159             # nelem = ceil( ( max - min ) / spacing )
160              
161             ( MIN | MAX | SPACING ),
162             sub {
163             local $_ = wrap_attrs_ro( {
164             force_extrema => $_->has( 'force_extrema' )
165             ? $_->force_extrema
166             : 0,
167             center => ( $_->max + $_->min ) / 2,
168             spacing => $_->spacing,
169             rangew => ( $_->max - $_->min ),
170             } );
171              
172             $ArgBuild{ ( CENTER | RANGEW | SPACING ) }->();
173             },
174              
175             #----------------------------------------
176              
177             # The sequence exactly covers [ min, min + nelem + spacing ]
178             ( MIN | NELEM | SPACING ),
179             sub {
180             {
181             elements => build_sequence( {
182             args => $_,
183             min => $_->min,
184             max => $_->min + ( $_->nelem - 1 ) * $_->spacing,
185             nelem => $_->nelem,
186             spacing => $_->spacing,
187             } ) };
188             },
189              
190             # The sequence exactly covers [ max - nelem + spacing, max ]
191             ( MAX | NELEM | SPACING ),
192             sub {
193             {
194             elements => build_sequence( {
195             args => $_,
196             min => $_->max - ( $_->nelem - 1 ) * $_->spacing,
197             max => $_->max,
198             nelem => $_->nelem,
199             spacing => $_->spacing,
200             } ) };
201             },
202              
203             #----------------------------------------
204             # The sequence exactly covers [ MIN = center - spacing * (nelem-1) / 2, MIN + nelem * spacing ]
205              
206             ( CENTER | SPACING | NELEM ),
207             sub {
208              
209             my $half_width = $_->spacing * ( $_->nelem - 1 ) / 2;
210              
211             {
212             elements => build_sequence( {
213             args => $_,
214             min => $_->center - $half_width,
215             max => $_->center + $half_width,
216             nelem => $_->nelem,
217             spacing => $_->spacing,
218             } ) };
219              
220             },
221              
222             # spacing = range_width / nelem
223             # The sequence covers [ MIN=center - width/2, MIN + (nelem-1) * spacing ]
224             ( CENTER | RANGEW | NELEM ),
225             sub {
226             my $spacing = $_->rangew / ( $_->nelem - 1 );
227              
228             {
229             elements => build_sequence( {
230             args => $_,
231             min => $_->center - $_->rangew / 2,
232             max => $_->center + $_->rangew / 2,
233             nelem => $_->nelem,
234             spacing => $spacing,
235             } ) };
236              
237             },
238              
239             # spacing = range_width / nelem
240             # The sequence covers [ MIN=center - width/2, MIN + (nelem-1) * spacing ]
241             ( CENTER | RANGEW | SPACING ),
242             sub {
243             my $nelem = ( $_->rangew / $_->spacing )->bceil + 1;
244              
245             {
246             elements => build_sequence( {
247             args => $_,
248             min => $_->center - ( $nelem - 1 ) / 2 * $_->spacing,
249             max => $_->center + ( $nelem - 1 ) / 2 * $_->spacing,
250             nelem => $nelem,
251             spacing => $_->spacing,
252             } ) };
253              
254             },
255              
256             # range_width is max of ( center - min, max - center )
257             # spacing = range_width / (nelem-1)
258             # The sequence covers [ MIN=center - width/2, MIN + (nelem-1) * spacing ]
259             ( CENTER | SOFT_MIN | SOFT_MAX | NELEM ),
260             sub {
261             my $hw0 = $_->center - $_->soft_min;
262             my $hw1 = $_->soft_max - $_->center;
263              
264             local $_ = wrap_attrs_ro( {
265             force_extrema => $_->has( 'force_extrema' )
266             ? $_->force_extrema
267             : 0,
268             center => $_->center,
269             rangew => 2 * ( $hw0 > $hw1 ? $hw0 : $hw1 ),
270             nelem => $_->nelem,
271             } );
272             $ArgBuild{ ( CENTER | RANGEW | NELEM ) }->();
273              
274             },
275              
276             # rangew is max of ( center - min, max - center )
277             # spacing = rangew / (nelem-1)
278             # The sequence covers [ MIN=center - width/2, MIN + (nelem-1) * spacing ]
279             ( CENTER | SOFT_MIN | SOFT_MAX | SPACING ),
280             sub {
281              
282             my $hw0 = $_->center - $_->soft_min;
283             my $hw1 = $_->soft_max - $_->center;
284              
285             local $_ = wrap_attrs_ro( {
286             force_extrema => $_->has( 'force_extrema' )
287             ? $_->force_extrema
288             : 0,
289             center => $_->center,
290             spacing => $_->spacing,
291             rangew => 2 * ( $hw0 > $hw1 ? $hw0 : $hw1 ),
292             } );
293              
294             $ArgBuild{ ( CENTER | RANGEW | SPACING ) }->();
295             },
296              
297             #----------------------------------------
298              
299             # The sequence is anchored at min and covers [ min, soft_max ]
300             ( MIN | SOFT_MAX | SPACING ),
301             sub {
302             my $nelem = ( ( $_->soft_max - $_->min ) / $_->spacing )->bceil + 1;
303              
304             {
305             elements => build_sequence( {
306             args => $_,
307             min => $_->min,
308             max => $_->min + $nelem * $_->spacing,
309             nelem => $nelem,
310             spacing => $_->spacing,
311             } ) };
312             },
313              
314              
315             # The sequence is anchored at max and covers [ soft_min, max ]
316             ( SOFT_MIN | MAX | SPACING ),
317             sub {
318             my $nelem = ( ( $_->max - $_->soft_min ) / $_->spacing )->bceil + 1;
319             {
320             elements => build_sequence( {
321             args => $_,
322             min => $_->max - ( $nelem - 1 ) * $_->spacing,
323             max => $_->max,
324             nelem => $nelem,
325             spacing => $_->spacing,
326             } ) };
327             },
328              
329             #----------------------------------------
330             # cover [min,max] with alignment
331             ( MIN | MAX | SPACING | ALIGN ),
332             sub {
333             my ( $P, $f ) = $_->align->@*;
334             my $E0 = $P - $f * $_->spacing;
335             my $imin = ( ( $_->min - $E0 ) / $_->spacing )->bfloor;
336             my $imax = ( ( $_->max - $E0 ) / $_->spacing )->bceil;
337             my $nelem = $imax - $imin + 1;
338             my $min = $E0 + $imin * $_->spacing;
339              
340             {
341             elements => build_sequence( {
342             args => $_,
343             min => $min,
344             max => $min + ( $nelem - 1 ) * $_->spacing,
345             nelem => $nelem,
346             spacing => $_->spacing,
347             } ) };
348             },
349              
350             ( MIN | MAX | NELEM | ALIGN ),
351             sub {
352             parameter_constraint->throw( 'nelem > 2' )
353             unless $_->nelem > 2;
354              
355             my ( $P, $f ) = $_->align->@*;
356             my $spacing = ( $_->max - $_->min ) / ( $_->nelem - 2 );
357             my $E0 = $P - $f * $spacing;
358             my $imin = ( ( $_->min - $E0 ) / $spacing )->bfloor;
359             # my $imax = ( ( $_->max - $E0 ) / $spacing )->bceil;
360             my $min = $E0 + $imin * $spacing;
361              
362             {
363             elements => build_sequence( {
364             args => $_,
365             min => $min,
366             max => $min + ( $_->nelem - 1 ) * $spacing,
367             nelem => $_->nelem,
368             spacing => $spacing,
369             } ) };
370             },
371              
372             #----------------------------------------
373              
374             );
375              
376             around BUILDARGS => buildargs_factory(
377             map => \%ArgMap,
378             build => \%ArgBuild,
379             xvalidate => \@ArgsCrossValidate,
380             );
381              
382             1;
383              
384             #
385             # This file is part of CXC-Number
386             #
387             # This software is Copyright (c) 2019 by Smithsonian Astrophysical Observatory.
388             #
389             # This is free software, licensed under:
390             #
391             # The GNU General Public License, Version 3, June 2007
392             #
393              
394             __END__
395              
396             =pod
397              
398             =for :stopwords Diab Jerius Smithsonian Astrophysical Observatory Extrema spacing extrema
399             extremum fiducial nelem
400              
401             =head1 NAME
402              
403             CXC::Number::Sequence::Linear - Numeric Sequence with Equal Spacing
404              
405             =head1 VERSION
406              
407             version 0.13
408              
409             =head1 SYNOPSIS
410              
411             use CXC::Number::Sequence::Linear;
412              
413             $sequence = CXC::Number::Sequence::Linear->new( %options );
414             $values = $sequence->sequence;
415              
416             =head1 DESCRIPTION
417              
418             B<CXC::Number::Sequence::Linear> constructs a finite sequence of
419             increasing, equally spaced, numbers.
420              
421             It subclasses L<CXC::Number::Sequence>, so see documentation for that
422             class for additional methods.
423              
424             Sequence extrema may be specified or may be calculated from other
425             parameters, such as spacing, the number of elements, or a sequence center,
426             and the sequence may be aligned on a fiducial value.
427              
428             Sequence extrema may be I<hard>, indicating that the sequence must exactly
429             cover the extrema, or I<soft>, indicating that the sequence may cover a
430             larger range. Usually the combination of parameters will uniquely
431             determine whether an extremum is soft or hard, but in some cases soft
432             bounds must be explicitly labelled as soft, requiring use of the
433             C<soft_min> and C<soft_max> parameters.
434              
435             A full description of the available parameters may be found in the
436             description of the constructor L</new>.
437              
438             =head2 Valid Parameter Combinations
439              
440             I<Note:> If the sequence extrema are equal to the range bounds, then
441             the sequence B<exactly> covers the range. Otherwise, the sequence
442             will cover the range, but may extend beyond one or more of the
443             extrema.
444              
445             =over
446              
447             =item C<min>, C<max>, C<nelem>.
448              
449             The sequence exactly covers the specified range.
450              
451             =item C<min>, C<max>, C<spacing>
452              
453             If an integral multiple of C<spacing> fits within the range, the sequence exactly
454             covers it, otherwise the sequence is centered on the range.
455              
456             =item C<min>, C<nelem>, C<spacing>
457              
458             =item C<max>, C<nelem>, C<spacing>
459              
460             The number of elements is chosen to exactly covers the calculated range.
461              
462             =item C<min>, C<soft_max>, C<spacing>
463              
464             =item C<soft_min>, C<max>, C<spacing>
465              
466             The hard extremum is as specified. The number of elements is chosen to
467             cover the specified range.
468              
469             =item C<center>, C<spacing>, C<nelem>
470              
471             The sequence is centered as specified and exactly covers the range.
472              
473             =item C<center>, C<rangew>, C<nelem>
474              
475             The sequence is centered as specified and exactly covers the range.
476              
477             =item C<center>, C<rangew>, C<spacing>
478              
479             The sequence is centered as specified and covers the range.
480              
481             =item C<center>, C<soft_min>, C<soft_max>, C<nelem>
482              
483             =item C<center>, C<soft_min>, C<soft_max>, C<spacing>
484              
485             The sequence is centered on the specified center and covers the
486             specified range.
487              
488             =item C<min>, C<max>, C<spacing>, C<align>
489              
490             =item C<min>, C<max>, C<nelem>, C<align>
491              
492             The sequence covers the specified range and is aligned so that the
493             specified alignment point is at the specified relative position between
494             elements.
495              
496             =back
497              
498             =head1 INTERNALS
499              
500             =for Pod::Coverage BUILDARGS
501              
502             =head1 Methods
503              
504             =head2 C<new>
505              
506             $sequence = CXC::Number::Sequence::Linear->new( %attr );
507              
508             Construct a linear spaced sequence. The available attributes are
509             those for the parent constructor in L<CXC::Number::Sequence>, as well
510             as the following:
511              
512             =over
513              
514             =item C<force_extrema> I<Boolean>
515              
516             Sometimes the extrema of the sequence will not exactly match what was
517             specified because of round-off error. If this option is true, then
518             sequences extrema will be set to the specified values. This may result
519             in spacings which are slightly different in size (the exact spacings
520             are available via the C<spacing> method).
521              
522             =item C<min>
523              
524             =item C<soft_min>
525              
526             The minimum value that the sequence should cover.
527             Use C<soft_min> to disambiguate hard from soft limits as documented above.
528              
529             =item C<max>
530              
531             =item C<soft_max>
532              
533             The maximum value that the sequence should cover.
534             Use C<soft_max> to disambiguate hard from soft limits as documented above.
535              
536             =item C<center>
537              
538             The center of the sequence. If there are an odd number of elements, this will
539             be the center element, otherwise it will be the average of the middle two.
540              
541             =item C<rangew>
542              
543             The width of the range to be covered by the sequence.
544              
545             =item C<C<nelem>>
546              
547             The number of elements in the sequence
548              
549             =item C<spacing>
550              
551             The distance between elements in the sequence.
552              
553             =item C<align> = I<[ $P, $f ]>
554              
555             The sequence will be aligned such that the fiducial value C<$P> is located
556             at the fractional position C<$f> between elements. C<$P> need not be in
557             the range of data covered by the sequence.
558              
559             For example, to align the sequence such that C<0> falls exactly half way between
560             two elements, even though the generated sequence doesn't include C<0>:
561              
562             use Data::Dump;
563             use aliased 'CXC::Number::Sequence::Linear';
564             dd Linear->new(
565             min => 5.1,
566             max => 8,
567             spacing => 1,
568             align => [ 0, 0.5 ],
569             )->elements;
570              
571             results in
572              
573             [4.5, 5.5, 6.5, 7.5, 8.5]
574              
575             =back
576              
577             If an inconsistent set of parameters is passed, C<new> will throw an exception of class
578             C<CXC::Number::Sequence::Failure::parameter::interface>.
579              
580             If an unknown parameter is passed, C<new> will throw an exception of class
581             C<CXC::Number::Sequence::Failure::parameter::unknown>.
582              
583             If a parameter value is illegal (e.g., a negative spacing),
584             or a combination of values is illegal ( e.g. C<< min > max >> ), C<new>
585             will throw an exception of class
586             C<CXC::Number::Sequence::Failure::parameter::constraint>.
587              
588             =head1 SUPPORT
589              
590             =head2 Bugs
591              
592             Please report any bugs or feature requests to bug-cxc-number@rt.cpan.org or through the web interface at: L<https://rt.cpan.org/Public/Dist/Display.html?Name=CXC-Number>
593              
594             =head2 Source
595              
596             Source is available at
597              
598             https://gitlab.com/djerius/cxc-number
599              
600             and may be cloned from
601              
602             https://gitlab.com/djerius/cxc-number.git
603              
604             =head1 SEE ALSO
605              
606             Please see those modules/websites for more information related to this module.
607              
608             =over 4
609              
610             =item *
611              
612             L<CXC::Number|CXC::Number>
613              
614             =back
615              
616             =head1 AUTHOR
617              
618             Diab Jerius <djerius@cpan.org>
619              
620             =head1 COPYRIGHT AND LICENSE
621              
622             This software is Copyright (c) 2019 by Smithsonian Astrophysical Observatory.
623              
624             This is free software, licensed under:
625              
626             The GNU General Public License, Version 3, June 2007
627              
628             =cut