Family relationships

This was just a weekend coding exercise. :slight_smile: 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()

In Leopard (on my G5) I had to replace “txt” with tx. “txt” seems to be reserved – fails with access not allowed. Otherwise, neat, fun, and accurate. Slick parsing. :smiley:

Hmmm. :confused: Sounds like a name-space clash with a third-party OSAX, or even ” dare I say it? ” Script Debugger. :stuck_out_tongue:

I’m confused… :confused:

How do I use this and why?

Instuctions for use in post #1 paragraph 3 and in the dialogs that appear when you run the script. There’s an explanation of “most recent common ancestor” here.

In this forum, you don’t necessarily use a script so much as learn from or enjoy the code, offer improvements, or simply ignore it.

Ah yes, Nigel; it is Script Debugger. :rolleyes:

Hm, on my machine (10.5.8 PPC with Script Debugger 4.5.4) the script compiles flawlessly.

@Nigel: very neat work (as usual) :slight_smile: