File Coverage

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