This was just a weekend coding exercise. It was inspired by an e-mail exchange with my brother a few days ago and by the subsequent discovery in Wikipedia of a mathematical description of family relationships. (“Are we third or fourth cousins?” rather than “Why does my wife beat me up on pay day?”)
This script works out two people’s relationship to each other from the number of generations between them and their most recent common ancestor.
If you want to know, say, what relation you are to your cousin’s granddaughter, you think of your most recent common ancestor ” in this case, the grandparents shared by you and your cousin ” and type “grandson” (or “granddaughter” or “grandchild”) into one dialog to describe yourself and “great-great-granddaughter” into the other for your cousin’s granddaughter. The script then returns the fact that you’re first cousins twice removed. And so on. For completeness, it’s permissible for one or both of the people to be the common ancestor. Just enter “self”, “himself”, or “herself” into one of the dialogs.
The getRelationship() handler manipulates the numbers and returns the result and is, I believe, fairly solid. sussPerson() is just a crude input parser to supply the generation and sex data to it here and needs tightening up.
(* Return a description of the relationship between two relatives, given records containing how many generations they're removed from their most recent common ancestor and their sexes (1/2/3 = m/f/unknown). *)
on getRelationship({generation:Ga, sex:sexA}, {generation:Gb, sex:sexB})
-- http://en.wikipedia.org/wiki/Cousin#Mathematical_definitions
set x to min(Ga, Gb) -- Generations from the common ancestor to the nearer relative.
set y to |abs|(Ga - Gb) -- Generations between the two relatives.
set groupA to (((Ga > Gb) as integer) + 1) -- Index of the group for relative A in 'relationships' below.
set groupB to (3 - groupA) -- Index of the group for relative B ditto.
set txt to "A and B are "
if ((x = 0) and (y = 0)) then -- Both relatives are the "common ancestor"!
set txt to txt & "the same individual!"
else if ((x = 1) and (y = 0)) then -- Both relatives are children of the common ancestor.
set relationships to {"brother", "sister", "sibling"}
if (sexA = sexB) then
set txt to txt & item sexA of relationships & "s."
else
set txt to txt & item sexA of relationships & " and " & item sexB of relationships & "."
end if
else if (x < 2) then -- The relatives are of different generations, one or both being the common ancestor or a child.
set greats to getGreats(y - 2)
set relationships to item (((y > 1) as integer) + 1) of item (x + 1) of ¬
{{{{"father", "mother", "parent"}, {"son", "daughter", "child"}}, {{"grandfather", "grandmother", "grandparent"}, {"grandson", "granddaughter", "grandchild"}}}, {{{"uncle", "aunt", "(uncle or aunt)"}, {"nephew", "niece", "(nephew or niece)"}}, {{"great-uncle", "great-aunt", "great-(uncle or aunt)"}, {"great-nephew", "great-niece", "great-(nephew or niece)"}}}}
set txt to txt & greats & item sexA of item groupA of relationships & " and " & greats & item sexB of item groupB of relationships & "."
else -- Both relatives are two or more generations removed from the common ancestor.
set txt to txt & ordinalise(x - 1) & " cousins" & timesRemoved(y) & "."
end if
return txt
end getRelationship
(* Return a record of a relative's generation and sex based on user input. *)
on sussPerson(letter)
set dialogText to "What relation is person " & letter & " to the most recent common ancestor of A & B?
(Input can be anything ending with 'self', 'son', 'daughter', or 'child'.)"
repeat -- until input usable.
set input to text returned of (display dialog dialogText default answer "" with title "Family Relationships" with icon note)
-- Input parsing based on my own linguistic and spelling assumptions. Needs tightening up for serious use.
set person to {generation:(input does not end with "self") as integer, sex:3}
if ((input is "himself") or (input ends with "son")) then
set person's sex to 1
else if ((input is "herself") or (input ends with "daughter")) then
set person's sex to 2
end if
if (input contains "grand") then set person's generation to (person's generation) + (count input's words)
if ((person's sex < 3) or (input ends with "self") or (input ends with "child")) then exit repeat
end repeat
return person
end sussPerson
(* Return the lesser of two values. *)
on min(a, b)
if (b < a) then return b
return a
end min
(* Return the absolute (positive) value of a number. *)
on |abs|(n)
if (n < 0) then return -n
return n
end |abs|
(* Append an ordinal suffix to a number. *)
-- Adapted from Victor Yee (adapted from NG (adapted from Jason Bourque) & Paul Skinner)
on ordinalise(n)
set units to n mod 10
if (units > 3) or ((n - units) mod 100 is 10) or (units < 1) or (units mod 1 > 0) then return (n as text) & "th"
return (n as text) & item units of {"st", "nd", "rd"}
end ordinalise
(* Convert a number to a frequency adverb followed by " removed". *)
on timesRemoved(n)
if (n = 0) then return ""
if (n < 4) then return item n of {" once removed", " twice removed", " thrice removed"}
return " " & n & " times removed"
end timesRemoved
(* Return a string containing n iterations of "great-". *)
on getGreats(n)
set txt to ""
repeat n times
set txt to txt & "great-"
end repeat
return txt
end getGreats
(* The main handler (!) *)
on getFamilyRelationship()
ignoring case
getRelationship(sussPerson("A"), sussPerson("B"))
end ignoring
display dialog result with title "Family Relationships" with icon note
end getFamilyRelationship
getFamilyRelationship()