From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#
# This file is part of Software-Copyright
#
# This software is Copyright (c) 2022 by Dominique Dumont <dod@debian.org>.
#
# This is free software, licensed under:
#
# The GNU General Public License, Version 3, June 2007
#
$Software::Copyright::Statement::VERSION = '0.015';
use 5.20.0;
use Mouse;
use Carp;
use Time::localtime;
use feature qw/postderef signatures/;
no warnings qw/experimental::postderef experimental::signatures/;
use overload '""' => \&stringify;
use overload 'cmp' => \&compare;
use overload '==' => \&_equal;
use overload 'eq' => \&_equal;
has span => (
is => 'ro',
isa => 'Array::IntSpan',
required => 1 , # may be an empty span
);
sub range ($self) {
return scalar $self->span->get_range_list;
}
has owner => (
is => 'rw',
isa => 'Software::Copyright::Owner',
required => 1,
handles => {
map { $_ => $_ } qw/name email record identifier/
},
);
sub __clean_copyright ($c) {
$c =~ s/^&copy;\s*//g;
$c =~ s/\(c\)\s*//gi;
# remove space around dash between number (eg. 2003 - 2004 => 2003-2004)
$c =~ s/(\d+)\s*-\s*(?=\d+)/$1-/g;
# extract year from YY-MM-DD:hh:mm:ss format
$c =~ s/(\d{2,4}-\d\d-\d{2,4})[:\d]*/my @r = strptime($1); $r[5]+1900/gex;
# remove extra years inside range, e,g 2003- 2004- 2008 -> 2003- 2008
$c =~ s/(?<=\b\d{4})\s*-\s*\d{4}(?=\s*-\s*(\d{4})\b)//g;
# add space after a comma between years
$c =~ s/\b(\d{4}),?\s+([\S^\d])/$1, $2/g;
$c =~ s/\s+by\s+//g;
$c =~ s/(\\n)*all\s+rights?\s+reserved\.?(\\n)*\s*//gi; # yes there are literal \n
$c = '' if $c =~ /^\*No copyright/i;
$c =~ s/\(r\)//g;
# remove spurious characters at beginning or end of string
$c =~ s!^[\s,/*]+|[\s,#/*-]+$!!g;
$c =~ s/--/-/g;
$c =~ s!\s+\*/\s+! !;
# remove copyright word surrounded by non alpha char (like "@copyright{}");
$c =~ s/[^a-z0-9\s,.'"]+copyright[^a-z0-9\s,.'"]+//i;
# libuv1 has copyright like "2000, -present"
$c =~ s![,\s]*-present!'-'.(localtime->year() + 1900)!e;
# texlive-extra has year range like 2023-20**
$c =~ s!(\d+)-2\d\*\*!"$1-".(localtime->year() + 1900)!e;
# texlive-extra has year range like 2011-..
$c =~ s!(\d+)-\.+!"$1-".(localtime->year() + 1900)!e;
# cleanup markdown copyright
$c =~ s/\[([\w\s]+)\]\(mailto:([\w@.+-]+)\)/$1 <$2>/;
return $c;
}
sub __split_copyright ($c) {
my ($years,$owner) = $c =~ /^(\d\d[\s,\d-]+)(.*)/;
# say "undef year in $c" unless defined $years;
if (not defined $years) {
# try owner and years in reversed order (works also without year)
($owner,$years) = $c =~ m/(.*?)(\d\d[\s,\d-]+)?$/;
}
$owner //='';
my @data = defined $years ? split /(?<=\d)[,\s]+/, $years : ();
$owner =~ s/^[\s.,-]+|[\s,*-]+$//g;
return ($owner,@data);
}
around BUILDARGS => sub ($orig, $class, @args) {
my $c = __clean_copyright($args[0]);
my ($owner_str, @data) = __split_copyright($c);
my $span = Array::IntSpan->new();
my $owner = Software::Copyright::Owner->new($owner_str);
foreach my $year (@data) {
last if $year =~ /[^\d-]/; # bail-out
# take care of ranges written like 2002-3
$year =~ s/^(\d\d\d)(\d)-(\d)$/$1$2-$1$3/;
# take care of ranges written like 2014-15
$year =~ s/^(\d\d)(\d\d)-(\d\d)$/$1$2-$1$3/;
eval {
# the value stored in range is not used.
$span->set_range_as_string($year, $owner->identifier // 'unknown');
};
if ($@) {
warn "Invalid year span: '$year' found in statement '$c'\n";
}
}
$span->consolidate();
return $class->$orig({
span => $span,
owner => $owner,
}) ;
};
sub stringify ($self,$=1,$=1) {
my $range = $self->span->get_range_list;
return join (', ', grep { $_ } ($range, $self->owner));
}
sub compare ($self, $other, $swap) {
# we must force stringify before calling cmp
return "$self" cmp "$other";
}
sub _equal ($self, $other, $swap) {
# we must force stringify before calling eq
return "$self" eq "$other";
}
sub merge ($self, $other) {
if ($self->identifier eq $other->identifier ) {
$self->email($other->email) if $other->email;
$self->span->set_range_as_string(scalar $other->span->get_range_list, $other->identifier);
$self->span->consolidate();
}
else {
croak "Cannot merge statement with mismatching owners";
}
return $self;
}
sub add_years ($self, $range) {
$self->span->set_range_as_string($range, $self->owner->identifier);
$self->span->consolidate;
return $self;
}
sub contains($self, $other) {
return 0 unless $self->identifier eq $other->identifier;
my $span = Array::IntSpan->new;
$span->set_range_as_string(scalar $self->span->get_range_list, $self->identifier);
# now $span is a copy of $self->span. Merge $other-span.
$span->set_range_as_string(scalar $other->span->get_range_list, $self->identifier);
$span->consolidate;
# if other span is contained in self->span, the merged result is not changed.
return scalar $span->get_range_list eq scalar $self->span->get_range_list ? 1 : 0;
}
1;
# ABSTRACT: a copyright statement for one owner
__END__
=pod
=encoding UTF-8
=head1 NAME
Software::Copyright::Statement - a copyright statement for one owner
=head1 VERSION
version 0.015
=head1 SYNOPSIS
use Software::Copyright::Statement;
my $statement = Software::Copyright::Statement->new('2020,2021, Joe <joe@example.com>');
$statement->name; # => is "Joe"
$statement->email; # => is 'joe@example.com'
$statement->range; # => is '2020, 2021'
# merge records
$statement->merge(Software::Copyright::Statement->new('2022, Joe <joe@example.com>'));
$statement->range; # => is '2020-2022'
# update the year range
$statement->add_years('2015, 2016-2019')->stringify; # => is '2015-2022, Joe <joe@example.com>'
# stringification
my $string = "$statement"; # => is '2015-2022, Joe <joe@example.com>'
# test if a statement "contains" another one
my $st_2020 = Software::Copyright::Statement->new('2020, Joe <joe@example.com>');
$statement->contains($st_2020); # => is '1'
=head1 DESCRIPTION
This class holds one copyright statement, i.e. year range, name
and email of one copyright contributor.
On construction, a cleanup is done to make the statement more
standard. Here are some cleanup example:
2002-6 Joe => 2002-2006, Joe
2001,2002,2003,2004 Joe => 2001-2004, Joe
# found in markdown documents
2002 Joe mailto:joe@example.com => 2002, Joe <joe@example.com>
=head1 CONSTRUCTOR
The constructor can be called without argument or with a string
containing:
=over
=item *
a year range (optional)
=item *
a name (mandatory)
=item *
an email address (optional)
=back
E.g:
my $st = Software::Copyright::Statement->new();
my $st = Software::Copyright::Statement->new('2002, Joe <joe@example.com>');
=head1 Methods
=head2 name
Set or get owner's name
=head2 email
Set or get owner's name
=head2 owner
Returns a L<Software::Copyright::Owner> object. This object can be
used as a string.
=head2 merge
Merge 2 statements. Note that the 2 statements must belong to the same
owner (the name attributes must be identical).
See the Synopsis for an example.
This method returns C<$self>
=head2 add_years
Add a year range to the copyright owner. This method accepts year
ranges like "2020", "2018, 2020", "2016-2020,2022". White spaces are
ignored.
This method returns C<$self>
=head2 stringify
Returns a string containing a year range (if any), a name and email
(if any) of the copyright owner.
=head2 contains
Return 1 if the other statement is contained in current statement,
i.e. owner or record are identical and other year range is contained
in current year range.
For instance:
=over
=item *
C<2016, Joe> is contained in C<2014-2020, Joe>
=item *
C<2010, Joe> is B<not> contained in C<2014-2020, Joe>
=back
=head2 Operator overload
Operator C<""> is overloaded to call C<stringify>.
=head1 AUTHOR
Dominique Dumont
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2022 by Dominique Dumont <dod@debian.org>.
This is free software, licensed under:
The GNU General Public License, Version 3, June 2007
=cut