File Coverage

blib/lib/Software/Copyright/Statement.pm
Criterion Covered Total %
statement 127 128 99.2
branch 13 14 92.8
condition 1 2 50.0
subroutine 24 24 100.0
pod 4 6 66.6
total 169 174 97.1


line stmt bran cond sub pod time code
1             #
2             # This file is part of Software-Copyright
3             #
4             # This software is Copyright (c) 2022 by Dominique Dumont <dod@debian.org>.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU General Public License, Version 3, June 2007
9             #
10             package Software::Copyright::Statement;
11             $Software::Copyright::Statement::VERSION = '0.011';
12 3     3   1478 use 5.20.0;
  3         11  
13 3     3   15 use warnings;
  3         6  
  3         91  
14              
15 3     3   1013 use Mouse;
  3         60592  
  3         36  
16 3     3   2942 use Array::IntSpan;
  3         9380  
  3         89  
17 3     3   40 use Carp;
  3         5  
  3         200  
18 3     3   1598 use Software::Copyright::Owner;
  3         10  
  3         92  
19 3     3   1579 use Date::Parse;
  3         23337  
  3         402  
20 3     3   1025 use Time::localtime;
  3         12260  
  3         202  
21              
22 3     3   29 use feature qw/postderef signatures/;
  3         14  
  3         259  
23 3     3   17 no warnings qw/experimental::postderef experimental::signatures/;
  3         7  
  3         147  
24              
25 3     3   15 use overload '""' => \&stringify;
  3         8  
  3         19  
26 3     3   217 use overload 'cmp' => \&compare;
  3         6  
  3         14  
27 3     3   174 use overload '==' => \&_equal;
  3         6  
  3         11  
28 3     3   171 use overload 'eq' => \&_equal;
  3         6  
  3         11  
29              
30             has span => (
31             is => 'ro',
32             isa => 'Array::IntSpan',
33             required => 1 , # may be an empty span
34             );
35              
36 11     11 0 344 sub range ($self) {
  11         19  
  11         15  
37 11         38 return scalar $self->span->get_range_list;
38             }
39              
40             has owner => (
41             is => 'rw',
42             isa => 'Software::Copyright::Owner',
43             required => 1,
44             handles => {
45             map { $_ => $_ } qw/name email record identifier/
46             },
47             );
48              
49 126     126   183 sub __clean_copyright ($c) {
  126         181  
  126         174  
50 126         260 $c =~ s/^&copy;\s*//g;
51 1     1   9 $c =~ s/\(c\)\s*//gi;
  1         2  
  1         16  
  126         323  
52             # remove space around dash between number (eg. 2003 - 2004 => 2003-2004)
53 126         28165 $c =~ s/(\d+)\s*-\s*(?=\d+)/$1-/g;
54             # extract year from YY-MM-DD:hh:mm:ss format
55 126         303 $c =~ s/(\d{2,4}-\d\d-\d{2,4})[:\d]*/my @r = strptime($1); $r[5]+1900/gex;
  3         70  
  3         402  
56             # remove extra years inside range, e,g 2003- 2004- 2008 -> 2003- 2008
57 126         301 $c =~ s/(?<=\b\d{4})\s*-\s*\d{4}(?=\s*-\s*(\d{4})\b)//g;
58             # add space after a comma between years
59 126         1043 $c =~ s/\b(\d{4}),?\s+([\S^\d])/$1, $2/g;
60 126         318 $c =~ s/\s+by\s+//g;
61 126         257 $c =~ s/(\\n)*all\s+rights?\s+reserved\.?(\\n)*\s*//gi; # yes there are literal \n
62 126 100       371 $c = '' if $c =~ /^\*No copyright/i;
63 126         211 $c =~ s/\(r\)//g;
64             # remove spurious characters at beginning or end of string
65 126         754 $c =~ s!^[\s,/*]+|[\s,#/*-]+$!!g;
66 126         220 $c =~ s/--/-/g;
67 126         192 $c =~ s!\s+\*/\s+! !;
68             # remove copyright word surrounded by non alpha char (like "@copyright{}");
69 126         207 $c =~ s/\W+copyright\W+//;
70             # libuv1 has copyright like "2000, -present"
71 126         207 $c =~ s![,\s]*-present!'-'.(localtime->year() + 1900)!e;
  1         3  
72             # cleanup markdown copyright
73 126         332 $c =~ s/\[([\w\s]+)\]\(mailto:([\w@.+-]+)\)/$1 <$2>/;
74 126         319 return $c;
75             }
76              
77 137     137   10370 sub __split_copyright ($c) {
  137         246  
  137         188  
78 137         546 my ($years,$owner) = $c =~ /^(\d\d[\s,\d-]+)(.*)/;
79             # say "undef year in $c" unless defined $years;
80 137 100       355 if (not defined $years) {
81             # try owner and years in reversed order (works also without year)
82 24         181 ($owner,$years) = $c =~ m/(.*?)(\d\d[\s,\d-]+)?$/;
83             }
84              
85 137   50     292 $owner //='';
86              
87 137 100       572 my @data = defined $years ? split /(?<=\d)[,\s]+/, $years : ();
88 137         679 $owner =~ s/^[\s.,-]+|[\s,*-]+$//g;
89 137         493 return ($owner,@data);
90             }
91              
92             around BUILDARGS => sub ($orig, $class, @args) {
93             my $c = __clean_copyright($args[0]);
94             my ($owner_str, @data) = __split_copyright($c);
95              
96             my $span = Array::IntSpan->new();
97             my $owner = Software::Copyright::Owner->new($owner_str);
98              
99             foreach my $year (@data) {
100             last if $year =~ /[^\d-]/; # bail-out
101             # take care of ranges written like 2002-3
102             $year =~ s/^(\d\d\d)(\d)-(\d)$/$1$2-$1$3/;
103             # take care of ranges written like 2014-15
104             $year =~ s/^(\d\d)(\d\d)-(\d\d)$/$1$2-$1$3/;
105             eval {
106             # the value stored in range is not used.
107             $span->set_range_as_string($year, $owner->identifier // 'unknown');
108             };
109             if ($@) {
110             warn "Invalid year span: '$year' found in statement '$c'\n";
111             }
112             }
113             $span->consolidate();
114              
115             return $class->$orig({
116             span => $span,
117             owner => $owner,
118             }) ;
119             };
120              
121 218     218 1 1030 sub stringify ($self,$=1,$=1) {
  218         321  
  218         317  
  218         287  
  218         307  
122 218         585 my $range = $self->span->get_range_list;
123 218         2759 return join (', ', grep { $_ } ($range, $self->owner));
  436         2659  
124             }
125              
126 42     42 0 210 sub compare ($self, $other, $swap) {
  42         68  
  42         64  
  42         74  
  42         58  
127             # we must force stringify before calling cmp
128 42         82 return "$self" cmp "$other";
129             }
130              
131 7     7   1746 sub _equal ($self, $other, $swap) {
  7         12  
  7         12  
  7         13  
  7         17  
132             # we must force stringify before calling eq
133 7         16 return "$self" eq "$other";
134             }
135              
136 22     22 1 35 sub merge ($self, $other) {
  22         32  
  22         28  
  22         31  
137 22 50       48 if ($self->identifier eq $other->identifier ) {
138 22 100       49 $self->email($other->email) if $other->email;
139 22         395 $self->span->set_range_as_string(scalar $other->span->get_range_list, $other->identifier);
140 22         1060 $self->span->consolidate();
141             }
142             else {
143 0         0 croak "Cannot merge statement with mismatching owners";
144             }
145 22         452 return $self;
146             }
147              
148 5     5 1 19 sub add_years ($self, $range) {
  5         27  
  5         11  
  5         7  
149 5         22 $self->span->set_range_as_string($range, $self->owner->identifier);
150 5         437 $self->span->consolidate;
151 5         153 return $self;
152             }
153              
154 15     15 1 36 sub contains($self, $other) {
  15         23  
  15         19  
  15         53  
155 15 100       40 return 0 unless $self->identifier eq $other->identifier;
156              
157 14         36 my $span = Array::IntSpan->new;
158 14         173 $span->set_range_as_string(scalar $self->span->get_range_list, $self->identifier);
159             # now $span is a copy of $self->span. Merge $other-span.
160 14         499 $span->set_range_as_string(scalar $other->span->get_range_list, $self->identifier);
161 14         925 $span->consolidate;
162              
163             # if other span is contained in self->span, the merged result is not changed.
164 14 100       186 return scalar $span->get_range_list eq scalar $self->span->get_range_list ? 1 : 0;
165             }
166              
167             1;
168              
169             # ABSTRACT: a copyright statement for one owner
170              
171             __END__
172              
173             =pod
174              
175             =encoding UTF-8
176              
177             =head1 NAME
178              
179             Software::Copyright::Statement - a copyright statement for one owner
180              
181             =head1 VERSION
182              
183             version 0.011
184              
185             =head1 SYNOPSIS
186              
187             use Software::Copyright::Statement;
188              
189             my $statement = Software::Copyright::Statement->new('2020,2021, Joe <joe@example.com>');
190              
191             $statement->name; # => is "Joe"
192             $statement->email; # => is 'joe@example.com'
193             $statement->range; # => is '2020, 2021'
194              
195             # merge records
196             $statement->merge(Software::Copyright::Statement->new('2022, Joe <joe@example.com>'));
197             $statement->range; # => is '2020-2022'
198              
199             # update the year range
200             $statement->add_years('2015, 2016-2019')->stringify; # => is '2015-2022, Joe <joe@example.com>'
201              
202             # stringification
203             my $string = "$statement"; # => is '2015-2022, Joe <joe@example.com>'
204              
205             # test if a statement "contains" another one
206             my $st_2020 = Software::Copyright::Statement->new('2020, Joe <joe@example.com>');
207             $statement->contains($st_2020); # => is '1'
208              
209             =head1 DESCRIPTION
210              
211             This class holds one copyright statement, i.e. year range, name
212             and email of one copyright contributor.
213              
214             On construction, a cleanup is done to make the statement more
215             standard. Here are some cleanup example:
216              
217             2002-6 Joe => 2002-2006, Joe
218             2001,2002,2003,2004 Joe => 2001-2004, Joe
219             # found in markdown documents
220             2002 Joe mailto:joe@example.com => 2002, Joe <joe@example.com>
221              
222             =head1 CONSTRUCTOR
223              
224             The constructor can be called without argument or with a string
225             containing:
226              
227             =over
228              
229             =item *
230              
231             a year range (optional)
232              
233             =item *
234              
235             a name (mandatory)
236              
237             =item *
238              
239             an email address (optional)
240              
241             =back
242              
243             E.g:
244              
245             my $st = Software::Copyright::Statement->new();
246             my $st = Software::Copyright::Statement->new('2002, Joe <joe@example.com>');
247              
248             =head1 Methods
249              
250             =head2 name
251              
252             Set or get owner's name
253              
254             =head2 email
255              
256             Set or get owner's name
257              
258             =head2 owner
259              
260             Returns a L<Software::Copyright::Owner> object. This object can be
261             used as a string.
262              
263             =head2 merge
264              
265             Merge 2 statements. Note that the 2 statements must belong to the same
266             owner (the name attributes must be identical).
267              
268             See the Synopsis for an example.
269              
270             This method returns C<$self>
271              
272             =head2 add_years
273              
274             Add a year range to the copyright owner. This method accepts year
275             ranges like "2020", "2018, 2020", "2016-2020,2022". White spaces are
276             ignored.
277              
278             This method returns C<$self>
279              
280             =head2 stringify
281              
282             Returns a string containing a year range (if any), a name and email
283             (if any) of the copyright owner.
284              
285             =head2 contains
286              
287             Return 1 if the other statement is contained in current statement,
288             i.e. owner or record are identical and other year range is contained
289             in current year range.
290              
291             For instance:
292              
293             =over
294              
295             =item *
296              
297             C<2016, Joe> is contained in C<2014-2020, Joe>
298              
299             =item *
300              
301             C<2010, Joe> is B<not> contained in C<2014-2020, Joe>
302              
303             =back
304              
305             =head2 Operator overload
306              
307             Operator C<""> is overloaded to call C<stringify>.
308              
309             =head1 AUTHOR
310              
311             Dominique Dumont
312              
313             =head1 COPYRIGHT AND LICENSE
314              
315             This software is Copyright (c) 2022 by Dominique Dumont <dod@debian.org>.
316              
317             This is free software, licensed under:
318              
319             The GNU General Public License, Version 3, June 2007
320              
321             =cut