genealogy 1.5.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,180 +1,174 @@
1
1
  module Genealogy
2
+ # Module QueryMethods provides methods to run genealogy queries to retrive relatives by role. It's included by the genealogy enabled AR model
2
3
  module QueryMethods
3
4
  extend ActiveSupport::Concern
4
5
 
5
- # parents
6
+ include Constants
7
+
8
+ # @return [2-elements Array] father and mother
6
9
  def parents
7
10
  [father,mother]
8
11
  end
9
-
10
- # eligible
11
- [:father, :mother].each do |parent|
12
- define_method "eligible_#{parent}s" do
13
- if send(parent)
14
- []
15
- else
16
- self.genealogy_class.send("#{Genealogy::PARENT2SEX[parent]}s") - descendants - [self]
17
- end
18
- end
12
+ # @return [ActiveRecord]
13
+ def paternal_grandfather
14
+ father && father.father
15
+ end
16
+ # @return [ActiveRecord]
17
+ def paternal_grandmother
18
+ father && father.mother
19
+ end
20
+ # @return [ActiveRecord]
21
+ def maternal_grandfather
22
+ mother && mother.father
23
+ end
24
+ # @return [ActiveRecord]
25
+ def maternal_grandmother
26
+ mother && mother.mother
19
27
  end
20
28
 
21
- # grandparents
22
- [:father, :mother].each do |parent|
23
- [:father, :mother].each do |grandparent|
24
-
25
- # get one
26
- define_method "#{Genealogy::PARENT2LINEAGE[parent]}_grand#{grandparent}" do
27
- send(parent) && send(parent).send(grandparent)
28
- end
29
-
30
- # eligible
31
- define_method "eligible_#{Genealogy::PARENT2LINEAGE[parent]}_grand#{grandparent}s" do
32
- if send(parent) and send("#{Genealogy::PARENT2LINEAGE[parent]}_grand#{grandparent}").nil?
33
- send(parent).send("eligible_#{grandparent}s") - [self]
34
- else
35
- []
36
- end
37
- end
38
-
39
- end
40
-
41
- # get two by lineage
42
- define_method "#{Genealogy::PARENT2LINEAGE[parent]}_grandparents" do
43
- (send(parent) && send(parent).parents) || [nil,nil]
44
- end
29
+ # @return [2-elements Array] paternal_grandfather and paternal_grandmother
30
+ def paternal_grandparents
31
+ father && father.parents
32
+ end
45
33
 
34
+ # @return [2-elements Array] maternal_grandfather and maternal_grandmother
35
+ def maternal_grandparents
36
+ mother && mother.parents
46
37
  end
47
38
 
39
+ # @return [4-elements Array] paternal_grandfather, paternal_grandmother, maternal_grandfather, maternal_grandmother
48
40
  def grandparents
49
- result = []
50
- [:father, :mother].each do |parent|
51
- [:father, :mother].each do |grandparent|
52
- result << send("#{Genealogy::PARENT2LINEAGE[parent]}_grand#{grandparent}")
53
- end
54
- end
55
- # result.compact! if result.all?{|gp| gp.nil? }
56
- result
41
+ [paternal_grandfather, paternal_grandmother, maternal_grandfather, maternal_grandmother]
57
42
  end
58
43
 
44
+ # @return [8-elements Array] paternal_grandfather's father, paternal_grandmother's father, maternal_grandfather's father, maternal_grandmother's father, paternal_grandfather's mother, paternal_grandmother's mother, maternal_grandfather's mother, maternal_grandmother's mother
59
45
  def great_grandparents
60
- parents.compact.inject([]){|memo, parent| memo |= parent.grandparents}
61
- end
62
-
63
- # offspring
64
- def offspring(options = {})
65
- if spouse = options[:spouse]
66
- raise WrongSexException, "Problems while looking for #{self}'s offspring made with spouse #{spouse} who should not be a #{spouse.sex}." if spouse.sex == sex
46
+ grandparents.inject([]){|memo, gp| memo += (gp.try(:parents) || [nil,nil]) }.flatten
47
+ end
48
+
49
+ # @param [Hash] options
50
+ # @option options [Object] spouse to filter children by spouse
51
+ # @return [ActiveRecord::Relation] children
52
+ def children(options = {})
53
+ raise SexError, "Sex value not valid for #{self}. It's needed to look for children" unless gclass.sex_values.include? sex
54
+ result = gclass.where(SEX2PARENT[ssex] => self)
55
+ if options.keys.include? :spouse
56
+ check_indiv(spouse = options[:spouse],opposite_ssex)
57
+ result = result.where(SEX2PARENT[opposite_ssex] => spouse ) if spouse
67
58
  end
68
- result = case sex
69
- when sex_male_value
70
- if options.keys.include?(:spouse)
71
- children_as_father.with(spouse.try(:id))
72
- else
73
- children_as_father
74
- end
75
- when sex_female_value
76
- if options.keys.include?(:spouse)
77
- children_as_mother.with(spouse.try(:id))
78
- else
79
- children_as_mother
80
- end
81
- else
82
- raise WrongSexException, "Sex value not valid for #{self}"
83
- end
84
- result.to_a
85
- end
86
- alias_method :children, :offspring
87
-
88
- def eligible_offspring
89
- self.genealogy_class.all - ancestors - offspring - siblings - [self]
59
+ result
90
60
  end
91
- alias_method :eligible_children, :eligible_offspring
92
61
 
93
- # spouses
62
+ # @return [ActiveRecord::Relation] list of individuals with whom has had children
94
63
  def spouses
95
- parent_method = Genealogy::SEX2PARENT[Genealogy::OPPOSITESEX[sex_to_s.to_sym]]
96
- offspring.collect{|child| child.send(parent_method)}.uniq
97
- end
98
-
99
- def eligible_spouses
100
- self.genealogy_class.send("#{Genealogy::OPPOSITESEX[sex_to_s.to_sym]}s") - spouses
64
+ gclass.where(id: children.pluck("#{SEX2PARENT[opposite_ssex]}_id".to_sym).compact.uniq)
101
65
  end
102
66
 
103
- # siblings
67
+ # @param [Hash] options
68
+ # @option options [Symbol] half let you filter siblings. Possible values are:
69
+ # :father for paternal halfsiblings
70
+ # :mother for maternal halfsiblings
71
+ # :only for all halfsiblings
72
+ # :include for fullsiblings and halfsiblings
73
+ # @return [ActiveRecord::Relation] list of fullsiblings and/or halfsiblings
104
74
  def siblings(options = {})
105
- result = case options[:half]
75
+ spouse = options[:spouse]
76
+
77
+ result = gclass.where.not(id: id)
78
+ case options[:half]
106
79
  when nil # only full siblings
107
- unless parents.include?(nil)
108
- father.try(:offspring, :spouse => mother ).to_a
80
+ result.all_with(:parents).where(father_id: father, mother_id: mother)
81
+ when :father # common father
82
+ result = result.all_with(:father).where(father_id: father)
83
+ if spouse
84
+ check_indiv(spouse, :female)
85
+ result.where(mother_id: spouse)
86
+ elsif mother
87
+ result.where("#{gclass.mother_id_column} != ? or #{gclass.mother_id_column} is ?", mother_id, nil)
109
88
  else
110
- []
89
+ result
111
90
  end
112
- when :father # common father
113
- father.try(:offspring, options.keys.include?(:spouse) ? {:spouse => options[:spouse]} : {}).to_a - mother.try(:offspring).to_a
114
91
  when :mother # common mother
115
- mother.try(:offspring, options.keys.include?(:spouse) ? {:spouse => options[:spouse]} : {}).to_a - father.try(:offspring).to_a
92
+ result = result.all_with(:mother).where(mother_id: mother)
93
+ if spouse
94
+ check_indiv(spouse, :male)
95
+ result.where(father_id: spouse)
96
+ elsif father
97
+ result.where("#{gclass.father_id_column} != ? or #{gclass.father_id_column} is ?", father_id, nil)
98
+ else
99
+ result
100
+ end
116
101
  when :only # only half siblings
117
- siblings(:half => :include) - siblings
102
+ ids = siblings(half: :father).pluck(:id) | siblings(half: :mother).pluck(:id)
103
+ result.where(id: ids)
118
104
  when :include # including half siblings
119
- father.try(:offspring).to_a + mother.try(:offspring).to_a
105
+ result.where("#{gclass.father_id_column} == ? or #{gclass.mother_id_column} == ?", father_id, mother_id)
120
106
  else
121
- raise WrongOptionValueException, "Admitted values for :half options are: :father, :mother, false, true or nil"
107
+ raise ArgumentError, "Admitted values for :half options are: :father, :mother, false, true or nil"
122
108
  end
123
- result.uniq - [self]
124
- end
125
109
 
126
- def eligible_siblings
127
- self.genealogy_class.all - ancestors - siblings(:half => :include) - [self]
128
110
  end
129
111
 
130
- def half_siblings
131
- siblings(:half => :only)
132
- # todo: inprove with option :father and :mother
112
+ # siblings with option half: :only
113
+ # @see #siblings
114
+ def half_siblings(options = {})
115
+ siblings(options.merge(half: :only))
133
116
  end
134
117
 
135
- def paternal_half_siblings
136
- siblings(:half => :father)
118
+ # siblings with option half: :father
119
+ # @see #siblings
120
+ def paternal_half_siblings(options = {})
121
+ siblings(options.merge(half: :father))
137
122
  end
138
123
 
139
- def maternal_half_siblings
140
- siblings(:half => :mother)
124
+ # siblings with option half: :mother
125
+ # @see #siblings
126
+ def maternal_half_siblings(options = {})
127
+ siblings(options.merge(half: :mother))
141
128
  end
142
129
 
143
- alias_method :eligible_half_siblings, :eligible_siblings
144
- alias_method :eligible_paternal_half_siblings, :eligible_siblings
145
- alias_method :eligible_maternal_half_siblings, :eligible_siblings
146
-
147
- # ancestors
148
- def ancestors
149
- result = []
150
- remaining = parents.compact
151
- until remaining.empty?
152
- result << remaining.shift
153
- remaining += result.last.parents.compact
130
+ # get list of known ancestrors iterateing over parents
131
+ # @param [Hash] options
132
+ # @return [ActiveRecord::Relation] list of ancestors
133
+ def ancestors(options = {})
134
+ ids = []
135
+ remaining_ids = parents.compact.map(&:id)
136
+ until remaining_ids.empty?
137
+ ids << remaining_ids.shift
138
+ remaining_ids += gclass.find(ids.last).parents.compact.map(&:id)
154
139
  end
155
- result.uniq
140
+ gclass.where(id: ids)
156
141
  end
157
142
 
158
- # descendants
143
+
144
+ # get list of known descendants iterateing over children ...
145
+ # @return [ActiveRecord::Relation] list of descendants
159
146
  def descendants
160
- result = []
161
- remaining = offspring.to_a.compact
162
- until remaining.empty?
163
- result << remaining.shift
164
- remaining += result.last.offspring.to_a.compact
165
- # break if (remaining - result).empty? can be necessary in case of loop. Idem for ancestors method
147
+ ids = []
148
+ remaining_ids = children.map(&:id)
149
+ until remaining_ids.empty?
150
+ ids << remaining_ids.shift
151
+ remaining_ids += gclass.find(ids.last).children.pluck(:id)
152
+ # break if (remaining_ids - ids).empty? can be necessary in case of loop. Idem for ancestors method
166
153
  end
167
- result.uniq
154
+ gclass.where(id: ids)
168
155
  end
169
156
 
157
+ # @return [ActiveRecord::Relation] list of grandchildren
170
158
  def grandchildren
171
- offspring.inject([]){|memo,child| memo |= child.offspring}
159
+ result = children.inject([]){|memo,child| memo |= child.children}
172
160
  end
173
161
 
162
+ # @return [ActiveRecord::Relation] list of grat-grandchildren
174
163
  def great_grandchildren
175
- grandchildren.compact.inject([]){|memo,grandchild| memo |= grandchild.offspring}
176
- end
177
-
164
+ result = grandchildren.compact.inject([]){|memo,grandchild| memo |= grandchild.children}
165
+ end
166
+
167
+ # list of uncles and aunts iterating through parents' siblings
168
+ # @param [Hash] options
169
+ # @option options [Symbol] lineage to filter by lineage: :paternal or :maternal
170
+ # @option options [Symbol] half to filter by half siblings (see #siblings)
171
+ # @return [ActiveRecord::Relation] list of uncles and aunts
178
172
  def uncles_and_aunts(options={})
179
173
  relation = case options[:lineage]
180
174
  when :paternal
@@ -184,86 +178,71 @@ module Genealogy
184
178
  else
185
179
  parents
186
180
  end
187
-
188
- case options[:sex]
189
- when :male
190
- relation.compact.inject([]){|memo,parent| memo |= parent.siblings(half: options[:half]).select(&:is_male?)}
191
- when :female
192
- relation.compact.inject([]){|memo,parent| memo |= parent.siblings(half: options[:half]).select(&:is_female?)}
193
- else
194
- relation.compact.inject([]){|memo,parent| memo |= parent.siblings(half: options[:half])}
195
- end
181
+ ids = relation.compact.inject([]){|memo,parent| memo |= parent.siblings(half: options[:half]).pluck(:id)}
182
+ gclass.where(id: ids)
196
183
  end
197
184
 
185
+ # @see #uncles_and_aunts
198
186
  def uncles(options = {})
199
- uncles_and_aunts(sex: :male, lineage: options[:lineage], half: options[:half])
187
+ uncles_and_aunts(options).males
200
188
  end
201
189
 
202
- def aunts(options={})
203
- uncles_and_aunts(sex: :female, lineage: options[:lineage], half: options[:half])
190
+ # @see #uncles_and_aunts
191
+ def aunts(options = {})
192
+ uncles_and_aunts(options).females
204
193
  end
205
194
 
195
+ # @see #uncles_and_aunts
206
196
  def paternal_uncles(options = {})
207
- uncles(sex: :male, lineage: :paternal, half: options[:half])
197
+ uncles(options.merge(lineage: :paternal))
208
198
  end
209
199
 
200
+ # @see #uncles_and_aunts
210
201
  def maternal_uncles(options = {})
211
- uncles(sex: :male, lineage: :maternal, half: options[:half])
202
+ uncles(options.merge(lineage: :maternal))
212
203
  end
213
204
 
205
+ # @see #uncles_and_aunts
214
206
  def paternal_aunts(options = {})
215
- aunts(lineage: :paternal, half: options[:half])
207
+ aunts(options.merge(lineage: :paternal))
216
208
  end
217
209
 
210
+ # @see #uncles_and_aunts
218
211
  def maternal_aunts(options = {})
219
- aunts(sex: :female, lineage: :maternal, half: options[:half])
212
+ aunts(options.merge(lineage: :maternal))
220
213
  end
221
214
 
222
- def cousins(options = {}, uncle_aunt_options = {})
223
- uncles_and_aunts(uncle_aunt_options).compact.inject([]){|memo,parent_sibling| memo |= parent_sibling.offspring}
215
+ # @param [Hash] options
216
+ # @option options [Symbol] lineage to filter uncles by lineage: :paternal or :maternal
217
+ # @option options [Symbol] half to filter uncles (see #siblings)
218
+ # @return [ActiveRecord::Relation] list of uncles and aunts' children
219
+ def cousins(options = {})
220
+ ids = uncles_and_aunts(options).inject([]){|memo,uncle| memo |= uncle.children.pluck(:id)}
221
+ gclass.where(id: ids)
224
222
  end
225
223
 
226
- def nieces_and_nephews(options = {}, sibling_options = {})
227
- case options[:sex]
228
- when :male
229
- siblings(sibling_options).inject([]){|memo,sib| memo |= sib.offspring}.select(&:is_male?)
230
- when :female
231
- siblings(sibling_options).inject([]){|memo,sib| memo |= sib.offspring}.select(&:is_female?)
232
- else
233
- siblings(sibling_options).inject([]){|memo,sib| memo |= sib.offspring}
234
- end
224
+ # @param [Hash] options
225
+ # @option options [Symbol] half to filter siblings (see #siblings)
226
+ # @return [ActiveRecord::Relation] list of nieces and nephews
227
+ def nieces_and_nephews(options = {})
228
+ ids = siblings(options).inject([]){|memo,sib| memo |= sib.children.pluck(:id)}
229
+ gclass.where(id: ids)
235
230
  end
236
231
 
237
- def nephews(options = {}, sibling_options = {})
238
- nieces_and_nephews(options.merge({sex: :male}), sibling_options)
232
+ # @see #nieces_and_nephews
233
+ def nephews(options = {})
234
+ nieces_and_nephews.males
239
235
  end
240
236
 
241
- def nieces(options = {}, sibling_options = {})
242
- nieces_and_nephews(options.merge({sex: :female}), sibling_options)
243
- end
244
-
245
- def family(options = {})
246
- res = [self] | siblings | parents | offspring
247
- res |= [current_spouse] if self.class.current_spouse_enabled
248
- res |= case options[:half]
249
- when nil
250
- []
251
- when :include
252
- half_siblings
253
- when :father
254
- paternal_half_siblings
255
- when :mother
256
- maternal_half_siblings
257
- else
258
- raise WrongOptionValueException, "Admitted values for :half options are: :father, :mother, :include, nil"
259
- end
260
- res = offspring.inject(res){|memo,child| memo |= child.parents} #add spouses
261
-
262
- res += [grandparents + grandchildren + uncles_and_aunts + nieces_and_nephews].flatten if options[:extended]
263
-
264
- res.compact
237
+ # @see #nieces_and_nephews
238
+ def nieces(options = {})
239
+ nieces_and_nephews.females
265
240
  end
266
241
 
242
+ # family hash with roles as keys? :spouse and individuals as values. Defaults roles are :father, :mother, :children, :siblings and current_spouse if enabled
243
+ # @option options [Symbol] half to filter siblings (see #siblings)
244
+ # @option options [Boolean] extended to include roles for grandparents, grandchildren, uncles, aunts, nieces, nephews and cousins
245
+ # @return [Hash] family hash with roles as keys? :spouse and individuals as values.
267
246
  def family_hash(options = {})
268
247
  roles = [:father, :mother, :children, :siblings]
269
248
  roles += [:current_spouse] if self.class.current_spouse_enabled
@@ -277,79 +256,58 @@ module Genealogy
277
256
  when :mother
278
257
  [:maternal_half_siblings]
279
258
  else
280
- raise WrongOptionValueException, "Admitted values for :half options are: :father, :mother, :include, nil"
259
+ raise ArgumentError, "Admitted values for :half options are: :father, :mother, :include, nil"
281
260
  end
282
- roles += [:paternal_grandfather, :paternal_grandmother, :maternal_grandfather, :maternal_grandmother, :grandchildren, :uncles_and_aunts, :nieces_and_nephews] if options[:extended]
283
- h = {}
284
- roles.each{|role| h[role] = self.send(role)}
285
- h
286
- end
287
-
288
- def extended_family(options = {})
289
- family(options.merge(:extended => true))
261
+ roles += [:paternal_grandfather, :paternal_grandmother, :maternal_grandfather, :maternal_grandmother, :grandchildren, :uncles_and_aunts, :nieces_and_nephews, :cousins] if options[:extended] == true
262
+ roles.inject({}){|res,role| res.merge!({role => self.send(role)})}
290
263
  end
291
264
 
265
+ # family_hash with option extended: :true
266
+ # @see #family_hash
292
267
  def extended_family_hash(options = {})
293
- family_hash(options.merge(:extended => true))
268
+ family_hash(options.merge(extended: true))
294
269
  end
295
270
 
296
- def sex_to_s
297
- case sex
298
- when sex_male_value
299
- 'male'
300
- when sex_female_value
301
- 'female'
302
- else
303
- raise WrongSexException, "Sex value not valid for #{self}"
304
- end
305
- end
306
-
307
- def is_female?
308
- return female? if respond_to?(:female?)
309
- sex == sex_female_value
310
- end
311
-
312
- def is_male?
313
- return male? if respond_to?(:male?)
314
- sex == sex_male_value
315
- end
316
-
317
- def birth
318
- self.send("#{genealogy_class.birth_date_column}")
271
+ # family individuals
272
+ # @return [Array]
273
+ # @see #family_hash
274
+ def family(options = {})
275
+ hash = family_hash(options)
276
+ hash.keys.inject([]){|tot,k| tot << hash[k] }.compact.flatten
319
277
  end
320
278
 
321
- def death
322
- self.send("#{genealogy_class.death_date_column}")
279
+ # family with option extended: :true
280
+ # @see #family
281
+ def extended_family(options = {})
282
+ family(options.merge(extended: true))
323
283
  end
324
284
 
325
- def age(options={})
326
- birth_date = birth
327
- death_date = death
328
- return if birth_date.nil?
329
-
330
- current = options[:end_date] ? DateTime.parse(options[:end_date]) : death_date || Time.zone.now
331
- years = current.year - birth_date.year
332
-
333
- if options[:measurement] == :years || !options[:measurement]
334
- return options[:string] ? "#{years} years" : years
335
- end
336
-
337
- months = current.month - birth_date.month
338
- months += 12 if months < 0
339
-
340
- if options[:measurement] == :months
341
- return options[:string] ? "#{years} years and #{months} months" : (years * 12) + months
342
- end
343
- return years
344
- end
285
+ private
345
286
 
346
287
  module ClassMethods
288
+ # all male individuals
289
+ # @return [ActiveRecord::Relation]
347
290
  def males
348
- where(sex_column => sex_male_value)
291
+ where(sex: sex_male_value)
349
292
  end
293
+ # all female individuals
294
+ # @return [ActiveRecord::Relation]
350
295
  def females
351
- where(sex_column => sex_female_value)
296
+ where(sex: sex_female_value)
352
297
  end
298
+ # all individuals individuals having relative with specified role
299
+ # @return [ActiveRecord, ActiveRecord::Relation]
300
+ def all_with(role)
301
+ case role
302
+ when :father
303
+ where.not(father_id: nil)
304
+ when :mother
305
+ where.not(mother_id: nil)
306
+ when :parents
307
+ where.not(mother_id: nil,father_id: nil)
308
+ end
309
+ end
310
+
353
311
  end
354
312
 
355
313
  end