File Coverage

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