File Coverage

blib/lib/Genealogy/Relationship.pm
Criterion Covered Total %
statement 41 41 100.0
branch 8 8 100.0
condition n/a
subroutine 9 9 100.0
pod 4 4 100.0
total 62 62 100.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Genealogy::Relationship - calculate the relationship between two people
4              
5             =head1 SYNOPSIS
6              
7             use Genealogy::Relationship;
8             use Person; # Imaginary class modelling people
9              
10             my $rel = Genealogy::Relationship->new;
11              
12             my $grandfather = Person->new( ... );
13             my $father = Person->new( ... );
14             my $me = Person->new( ... );
15             my $aunt = Person->new( ... );
16             my $cousin = Person->new( ... );
17              
18             my $common_ancestor = $rel->get_most_recent_common_ancestor(
19             $me, $cousin,
20             );
21             say $common_ancestor->name; # Grandfather's name
22              
23             say $rel->get_relationship($me, $grandfather); # Grandson
24             say $rel->get_relationship($grandfather, $me); # Grandfather
25              
26             say $rel->get_relationship($father, $cousin); # Uncle
27             say $rel->get_relationship($cousin, $father); # Niece
28              
29             =head1 DESCRIPTION
30              
31             This module makes it easy to calculate the relationship between two people.
32              
33             If you have a set of objects modelling your family tree, then you will
34             be able to use this module to get a description of the relationship between
35             any two people on that tree.
36              
37             The objects that you use with this module need to implement three methods:
38              
39             =over 4
40              
41             =item * parent
42              
43             This method should return the object which is the parent of the current
44             person.
45              
46             =item * id
47              
48             This method should return a unique identifier for the current person.
49             The identifier should be a number.
50              
51             =item * gender
52              
53             This method should return the gender of the current person. It should be
54             the character 'm' or 'f'.
55              
56             =back
57              
58             =head2 Limitations
59              
60             This module was born out of a need I had while creating
61             L. This leads to a couple of limitations
62             that I hope to remove at a later date.
63              
64             =over 4
65              
66             =item 1
67              
68             Each person in the tree is expected to have only one parent. This is, of
69             course, about half of the usual number. It's like that because for the line
70             of succession I'm tracing bloodlines and only one parent is ever going to
71             be significant.
72              
73             I realise that this is a significant limitation and I'll be thinking about
74             how to fix it as soon as possible.
75              
76             =item 2
77              
78             The table that I use to generate the relationship names only goes back five
79             generations - that's to fourth cousins (people who share great, great,
80             great grandparents with each other).
81              
82             This has, so far, been enough for my purposes, but I realise that more
83             coverage would be useful. I should probably move away from a table-based
84             approach and find a way to calculate the relationship names.
85              
86             =back
87              
88             =head2 Caching
89              
90             Calculating relationship names isn't at all different. But there can be a lot
91             of (simple and repetitive) work involved. This is particularly true if your
92             objects are based on database tables (as I found to my expense).
93              
94             If you're calculating a lot of relationships, then you should probably
95             consider putting a caching layer in front of C.
96              
97             =cut
98              
99             package Genealogy::Relationship;
100              
101 2     2   153837 use Moo;
  2         24519  
  2         8  
102 2     2   4389 use Types::Standard qw[Str HashRef];
  2         165815  
  2         25  
103 2     2   2099 use List::Util qw[first];
  2         5  
  2         246  
104 2     2   1279 use List::MoreUtils qw[firstidx];
  2         21792  
  2         18  
105              
106             our $VERSION = '0.0.5';
107              
108             has parent_field_name => (
109             is => 'ro',
110             isa => Str,
111             default => 'parent',
112             );
113              
114             has identifier_field_name => (
115             is => 'ro',
116             isa => Str,
117             default => 'id',
118             );
119              
120             has gender_field_name => (
121             is => 'ro',
122             isa => Str,
123             default => 'gender',
124             );
125              
126             has relationship_table => (
127             is => 'ro',
128             isa => HashRef,
129             builder => '_build_relationship_table',
130             );
131              
132             sub _build_relationship_table {
133             return {
134 2     2   11550 m => [
135             [ undef, 'Father', 'Grandfather', 'Great grandfather', 'Great, great grandfather', 'Great, great, great grandfather' ],
136             ['Son', 'Brother', 'Uncle', 'Great uncle', 'Great, great uncle', 'Great, great, great uncle' ],
137             ['Grandson', 'Nephew', 'First cousin', 'First cousin once removed', 'First cousin twice removed', 'First cousin three times removed' ],
138             ['Great grandson', 'Great nephew', 'First cousin once removed', 'Second cousin', 'Second cousin once removed', 'Seconc cousin twice removed' ],
139             ['Great, great grandson', 'Great, great nephew', 'First cousin twice removed', 'Second cousin once removed', 'Third cousin', 'Third cousin once removed' ],
140             ['Great, great, great grandson', 'Great, great, great nephew', 'First cousin three times removed', 'Second cousin twice removed', 'Third cousin once removed', 'Fourth cousin' ],
141             ],
142             f => [
143             [ undef, 'Mother', 'Grandmother', 'Great grandmother', 'Great, great grandmother', 'Great, great great grandmother' ],
144             ['Daughter', 'Sister', 'Aunt', 'Great aunt', 'Great, great aunt', 'Great, great, great aunt' ],
145             ['Granddaughter', 'Niece', 'First cousin', 'First cousin once removed', 'First cousin twice removed', 'First cousin three times removed' ],
146             ['Great granddaughter', 'Great niece', 'First cousin once removed', 'Second cousin', 'Second cousin once removed', 'Second cousin twice removed' ],
147             ['Great, great granddaughter', 'Great, great niece', 'First cousin twice removed', 'Second cousin once removed', 'Third cousin', 'Third cousin once removed' ],
148             ['Great, great, great granddaughter', 'Great, great, great niece', 'First cousin three times removed', 'Second cousin twice removed', 'Third cousin once removed', 'Fourth cousin' ],
149             ],
150             };
151             }
152              
153             =head1 Methods
154              
155             The following methods are defined.
156              
157             =head2 most_recent_common_ancestor
158              
159             Given two person objects, returns the person who is the most recent common
160             ancestor for the given people.
161              
162             =cut
163              
164             sub most_recent_common_ancestor {
165 5     5 1 585 my $self = shift;
166 5         12 my ($person1, $person2) = @_;
167              
168             # Are they the same person?
169 5 100       31 return $person1 if $person1->id == $person2->id;
170              
171 4         10 my @ancestors1 = ($person1, $self->get_ancestors($person1));
172 4         9 my @ancestors2 = ($person2, $self->get_ancestors($person2));
173              
174 4         9 for my $anc1 (@ancestors1) {
175 9         17 for my $anc2 (@ancestors2) {
176 17 100       57 return $anc1 if $anc1->id == $anc2->id;
177             }
178             }
179              
180 1         13 die "Can't find a common ancestor.\n";
181             }
182              
183             =head2 get_ancestors
184              
185             Given a person object, returns a list of person objects, one for each
186             ancestor of the given person.
187              
188             The first person in the list will be the person's parent and the last person
189             will be their most distant ancestor.
190              
191             =cut
192              
193             sub get_ancestors {
194 33     33 1 1565 my $self = shift;
195 33         60 my ($person) = @_;
196              
197 33         56 my @ancestors = ();
198              
199 33         101 while (defined ($person = $person->parent)) {
200 43         107 push @ancestors, $person;
201             }
202              
203 33         81 return @ancestors;
204             }
205              
206             =head2 get_relationship
207              
208             Given two person objects, returns a string containing a description of the
209             relationship between those two people.
210              
211             =cut
212              
213             sub get_relationship {
214 6     6 1 33 my $self = shift;
215 6         14 my ($person1, $person2) = @_;
216              
217 6         18 my ($x, $y) = $self->get_relationship_coords($person1, $person2);
218              
219 6         63 return $self->relationship_table->{$person1->gender}[$x][$y];
220             }
221              
222             =head2 get_relationship_coords
223              
224             Given two person objects, returns the "co-ordinates" of the relationship
225             between them.
226              
227             The relationship co-ordinates are a pair of integers. The first integer is
228             the number of generations between the first person and their most recent
229             common ancestor. The second integer is the number of generations between
230             the second person and their most recent common ancestor.
231              
232             =cut
233              
234             sub get_relationship_coords {
235 12     12 1 89 my $self = shift;
236 12         28 my ($person1, $person2) = @_;
237              
238             # If the two people are the same person, then return (0, 0).
239 12 100       68 return (0, 0) if $person1->id == $person2->id;
240              
241 11         49 my @ancestors1 = ($person1, $self->get_ancestors($person1));
242 11         24 my @ancestors2 = ($person2, $self->get_ancestors($person2));
243              
244 11         36 for my $i (0 .. $#ancestors1) {
245 26         49 for my $j (0 .. $#ancestors2) {
246 51 100       184 return ($i, $j) if $ancestors1[$i]->id == $ancestors2[$j]->id;
247             }
248             }
249              
250 1         19 die "Can't work out the relationship.\n";
251             }
252              
253             =head1 AUTHOR
254              
255             Dave Cross
256              
257             =head1 SEE ALSO
258              
259             perl(1)
260              
261             =head1 COPYRIGHT AND LICENSE
262              
263             Copyright (C) 2018-2020, Magnum Solutions Ltd. All Rights Reserved.
264              
265             This script is free software; you can redistribute it and/or modify it
266             under the same terms as Perl itself.
267              
268             =cut
269              
270             1;