File Coverage

lib/SemVer/V2/Strict.pm
Criterion Covered Total %
statement 89 89 100.0
branch 30 30 100.0
condition 21 26 80.7
subroutine 22 22 100.0
pod 9 9 100.0
total 171 176 97.1


line stmt bran cond sub pod time code
1             package SemVer::V2::Strict;
2 8     8   1541548 use strict;
  8         16  
  8         402  
3 8     8   40 use warnings;
  8         19  
  8         559  
4 8     8   46 use utf8;
  8         10  
  8         57  
5             our $VERSION = '0.18';
6              
7 8     8   1023 use constant PRE_RELEASE_FORMAT => qr/(?:-(?<pre_release>[a-zA-Z0-9.\-]+))?/;
  8         23  
  8         3506  
8 8     8   49 use constant BUILD_METADATA_FORMAT => qr/(?:\+(?<build_metadata>[a-zA-Z0-9.\-]+))?/;
  8         13  
  8         1019  
9 8         18 use constant VERSION_FORMAT =>
10 8     8   2086 qr/(?<major>[0-9]+)(?:\.(?<minor>[0-9]+))?(?:\.(?<patch>[0-9]+))?@{[PRE_RELEASE_FORMAT]}@{[BUILD_METADATA_FORMAT]}/;
  8         2055  
  8         34  
  8         3504  
11              
12 8     8   62 use List::Util qw/min max/;
  8         21  
  8         819  
13 8     8   66 use Scalar::Util qw/looks_like_number/;
  8         17  
  8         731  
14              
15             use overload (
16 8         80 q{""} => \&as_string,
17             q{<=>} => \&_cmp,
18             fallback => 1,
19 8     8   46 );
  8         12  
20              
21 62     62 1 243 sub major { shift->{major} }
22 48     48 1 128 sub minor { shift->{minor} }
23 46     46 1 135 sub patch { shift->{patch} }
24 47     47 1 579 sub pre_release { shift->{pre_release} }
25 22     22 1 97 sub build_metadata { shift->{build_metadata} }
26              
27             sub new {
28 47     47 1 744294 my $class = shift;
29 47         82 my $self = bless {} => $class;
30              
31 47 100       131 $self->_init_by_version_numbers if @_ == 0;
32 47 100       1436 $self->_init_by_version_string(@_) if @_ == 1;
33 43 100       1795 $self->_init_by_version_numbers(@_) if @_ >= 2;
34              
35 43         9785 return $self;
36             }
37              
38             sub _init_by_version_string {
39 51     51   272600 my ($self, $version) = @_;
40              
41 51 100       70 die 'Invalid format' unless $version =~ qr/^@{[VERSION_FORMAT]}$/;
  51         1350  
42              
43 45         498 $self->{major} = $+{major};
44 45   100     270 $self->{minor} = $+{minor} // 0;
45 45   100     190 $self->{patch} = $+{patch} // 0;
46 45         157 $self->{pre_release} = $+{pre_release};
47 45         155 $self->{build_metadata} = $+{build_metadata};
48             }
49              
50             sub _init_by_version_numbers {
51 1     1   246910 my ($self, $major, $minor, $patch, $pre_release, $build_metadata) = @_;
52              
53 1   50     42 $self->{major} = $major // 0;
54 1   50     5 $self->{minor} = $minor // 0;
55 1   50     4 $self->{patch} = $patch // 0;
56 1         2 $self->{pre_release} = $pre_release;
57 1         6 $self->{build_metadata} = $build_metadata;
58             }
59              
60             sub clean {
61 12     12 1 188603 my ($class, $version) = @_;
62              
63 12         98 $version =~ s/^\s*(.*?)\s*$/$1/; # trim
64 12         27 $version =~ s/^[=v]+//;
65              
66 12         15 return eval { $class->new($version)->as_string };
  12         27  
67             }
68              
69             sub sort {
70 1     1 1 254816 my ($class, @versions) = @_;
71 1         8 return sort { $class->new($a) <=> $class->new($b) } @versions;
  5         27  
72             }
73              
74             sub as_string {
75 14     14 1 592 my $self = shift;
76              
77 14         32 my $string = $self->major.'.'.$self->minor.'.'.$self->patch;
78 14 100       30 $string .= '-'.$self->pre_release if $self->pre_release;
79 14 100       28 $string .= '+'.$self->build_metadata if $self->build_metadata;
80              
81 14         72 return $string;
82             }
83              
84             sub _cmp {
85 17     17   7610 my ($self, $other) = @_;
86              
87 17 100       44 return $self->major <=> $other->major if $self->major != $other->major;
88 13 100       27 return $self->minor <=> $other->minor if $self->minor != $other->minor;
89 12 100       20 return $self->patch <=> $other->patch if $self->patch != $other->patch;
90 11         19 return _compare_pre_release($self->pre_release, $other->pre_release);
91             }
92              
93             sub _compare_pre_release {
94 11     11   18 my ($a, $b) = @_;
95              
96 11 100 100     67 return 1 if !defined $a && $b;
97 10 100 100     29 return -1 if $a && !defined $b;
98              
99 9 100 66     20 if ($a && $b) {
100 8         35 my @left = split /-|\./, $a;
101 8         21 my @right = split /-|\./, $b;
102 8         20 my $max = max(scalar @left, scalar @right);
103              
104 8         14 for (my $i = 0; $i < $max; ++$i) {
105 12   100     25 my $a = $left[$i] // 0;
106 12   50     26 my $b = $right[$i] // 0;
107              
108 12 100 100     36 if (looks_like_number($a) && looks_like_number($b)) {
109 4 100       16 return $a <=> $b if $a != $b;
110             }
111              
112 9         16 my $min = min(length $a, length $b);
113 9         13 for (my $n = 0; $n < $min; ++$n) {
114 25         32 my $c = substr($a, $n, 1) cmp substr($b, $n, 1);
115 25 100       50 return $c if $c != 0;
116             }
117             }
118             }
119              
120 3         10 return 0;
121             }
122              
123             1;
124             __END__
125              
126             =encoding utf-8
127              
128             =head1 NAME
129              
130             SemVer::V2::Strict - Semantic version v2.0 object for Perl
131              
132             =head1 SYNOPSIS
133              
134             use SemVer::V2::Strict;
135              
136             my $v1 = SemVer::V2::Strict->new('1.0.2');
137             my $v2 = SemVer::V2::Strict->new('2.0.0-alpha.10');
138              
139             if ($v1 < $v2) {
140             print "$v1 < $v2\n"; # => '1.0.2 < 2.0.0-alpha.10'
141             }
142              
143             =head1 DESCRIPTION
144              
145             This module subclasses version to create semantic versions, as defined by the L<Semantic Versioning 2.0.0|http://semver.org/spec/v2.0.0.html> Specification.
146              
147             =head1 METHODS
148              
149             =head2 CLASS METHODS
150              
151             =head3 C<new()>
152              
153             Create new empty C<SemVer::V2::Strict> instance.
154             C<SemVer::V2::Strict-E<gt>new()> equals C<SemVer::V2::Strict-E<gt>new('0.0.0')>.
155              
156             =head3 C<new($version_string)>
157              
158             Create new C<SemVer::V2::Strict> instance from a version string.
159             C<SemVer::V2::Strict-E<gt>new('1.0.0')> equals C<SemVer::V2::Strict-E<gt>new(1, 0, 0)>.
160              
161             =head3 C<new($major, $minor = 0, $patch = 0, $pre_release = undef, $build_metadata = undef)>
162              
163             Create new C<SemVer::V2::Strict> instance from version numbers.
164             C<SemVer::V2::Strict-E<gt>new('1.0.0-alpha+100')> equals C<SemVer::V2::Strict-E<gt>new(1, 0, 0, 'alpha', '100')>.
165              
166             =head3 C<clean($version_string)>
167              
168             Clean version string. Trim spaces and C<'v'> prefix.
169              
170             =head3 C<sort(... $version_string)>
171              
172             Sort version strings.
173              
174             =head2 METHODS
175              
176             =head3 C<major>
177              
178             Get the major version number.
179              
180             =head3 C<minor>
181              
182             Return the minor version number.
183              
184             =head3 C<patch>
185              
186             Return the patch version number.
187              
188             =head3 C<pre_release>
189              
190             Return the pre_release string.
191              
192             =head3 C<build_metadata>
193              
194             Return the build_metadata string.
195              
196             =head3 C<E<lt>=E<gt>>
197              
198             Compare two C<SemVer::V2::Strict> instances.
199              
200             =head3 C<"">
201              
202             Convert a C<SemVer::V2::Strict> instance to string.
203              
204             =head3 C<as_string()>
205              
206             Convert a C<SemVer::V2::Strict> instance to string.
207              
208             =head1 SEE ALSO
209              
210             =over
211              
212             =item * L<SemVer>
213              
214             =item * L<version>
215              
216             =back
217              
218             =head1 LICENSE
219              
220             The MIT License (MIT)
221              
222             Copyright (c) 2015-2019, 2025 Pine Mizune
223              
224             Permission is hereby granted, free of charge, to any person obtaining a copy
225             of this software and associated documentation files (the "Software"), to deal
226             in the Software without restriction, including without limitation the rights
227             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
228             copies of the Software, and to permit persons to whom the Software is
229             furnished to do so, subject to the following conditions:
230              
231             The above copyright notice and this permission notice shall be included in
232             all copies or substantial portions of the Software.
233              
234             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
235             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
236             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
237             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
238             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
239             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
240             THE SOFTWARE.
241              
242             =head1 ACKNOWLEDGEMENT
243              
244             C<SemVer::V2::Strict> is based from rosylilly's L<semver|https://github.com/rosylilly/semver>.
245             Thank you.
246              
247             =head1 AUTHOR
248              
249             Pine Mizune E<lt>pinemz@gmail.comE<gt>
250              
251             =cut
252