My version of Supertool script to calculate Birth and Death dates

I have developed my own version of a script for calculating birth and death dates based on the following information:

  • Known events of the current individual
  • Marriages
  • Information about children
  • Information about parents
  • Information about partners

In addition to the calculation, the script also automatically updates existing birth and death events. Only events without dates are updated by the script. Existing dates remain unchanged. Personally, this saved me a lot of time because I had around 2k births and around 4.5k deaths without dates.

I want to note that for the calculation, I only used years and ignored months and days. I believe that approximate calculations should not include months or days. However, someone might think differently. For this reason, I think the “Verify the Data” utility will mark my calculated dates as errors because if, for example, a person was born on 1800-03-05, and the calculation estimates that they died around after 1800, then we know that 1800 is equivalent to 1800-00-00, which is less than 1800-03-05. But for me personally, this is acceptable.

I don’t consider my script aesthetically pleasing, and it’s not optimized (this is my first experiense). I’m sharing it only because someone might need some fragments of it for personal tasks, even if they will not be used as intended.

[Gramps SuperTool script file]
version=1

[title]
Update birth and death dates

[description]
# This script is designed to automatically update event dates (birth and death) in a database based on other events indicating approximate times 
# of these events. It utilizes information about various event types (birth, death, marriage, etc.) as well as the roles of participants in these 
# events to determine approximate birth and death dates for individuals in the database. Additionally, the script automatically synchronizes 
# birth and death dates and updates the corresponding events in the database accordingly.
# The following information is utilized for calculating dates:
# - Known events of the current individual
# - Marriages
# - Information about children
# - Information about parents
# - Information about partners

[category]
People

[initial_statements]

# Initialization of constants for date modifiers and qualities
MOD_NONE = 0
MOD_BEFORE = 1
MOD_AFTER = 2
MOD_ABOUT = 3
MOD_RANGE = 4
MOD_SPAN = 5
MOD_TEXTONLY = 6
QUAL_NONE = 0
QUAL_ESTIMATED = 1
QUAL_CALCULATED = 2

# A note with comment inside like: "This event data was updated automatically with a SuperTool script"
auto_update_note = db.get_note_from_gramps_id("N1823")

# Dictionary to store calculated dates for each person
people_calculated_dates = {}

# Function to retrieve events from event proxies
def get_events(event_proxies):
    results = []
    for event_proxy in event_proxies:
        results.append(event_proxy.obj)
    return results

# Function to extract the year from a date object
def get_date(date):
    if date.is_regular() or (date.get_year_valid() and date.get_modifier() in [MOD_NONE, MOD_ABOUT]):
        return date.get_year()
    else:
        return None

# Function to retrieve the role of a person in an event
def get_role(person, db, referrer_handle, event_handle):
    person = db.get_person_from_handle(referrer_handle)
    eventref_list = person.get_event_ref_list()
    for eventref in eventref_list:
        if eventref.ref == event_handle:
            return eventref.role
    return None

# Function to get the larger of two values
def get_larger(a, b):
    if a is None and b is None:
        return None
    elif a is None:
        return b
    elif b is None:
        return a
    else:
        return a if a > b else b

# Function to get the smaller of two values
def get_less(a, b):
    if a is None and b is None:
        return None
    elif a is None:
        return b
    elif b is None:
        return a
    else:
        return a if a < b else b

# Function to update birth dates based on event type and role
def update_birth_dates(person_dates, date, offset_from, offset_to):
    if not person_dates["birth"]["has_exact_date"]:
        if offset_from is not None:
            person_dates["birth"]["from"] = get_larger(person_dates["birth"]["from"], date + offset_from)
        if offset_to is not None:
            person_dates["birth"]["to"] = get_less(person_dates["birth"]["to"], date + offset_to)

# Function to update death dates based on event type and role
def update_death_dates(person_dates, date, offset_from, offset_to):
    if not person_dates["death"]["has_exact_date"]:
        if offset_from is not None:
            person_dates["death"]["from"] = get_larger(person_dates["death"]["from"], date + offset_from)
        if offset_to is not None:
            person_dates["death"]["to"] = get_less(person_dates["death"]["to"], date + offset_to)

# Function to calculate events for a person
def calcPersonEvents(curr_person):
    person_calculated_dates = {
        "birth": {"from": None, "to": None, "has_exact_date": False}, 
        "death": {"from": None, "to": None, "has_exact_date": False}
    }
    person_events = get_events(curr_person.events)
    for event in person_events:
        date = event.get_date_object()
        person_obj = db.get_person_from_handle(curr_person.handle)
        role = get_role(person_obj, db, curr_person.handle, event.handle)
        date = get_date(date)
        if date is None:
            continue
        if event.get_type() == "Baptism" and role == "Primary":
            continue
        if event.get_type() in ["Burial", "Military Service"]:
            continue
        if event.get_type() == "Birth" and role in ["Primary"]:
            update_birth_dates(person_calculated_dates, date, 0, 0)
            person_calculated_dates["birth"]["has_exact_date"] = True
            update_death_dates(person_calculated_dates, date, 0, 100)
        elif event.get_type() == "Death" and role in ["Primary"]:
            update_death_dates(person_calculated_dates, date, 0, 0)
            person_calculated_dates["death"]["has_exact_date"] = True
            update_birth_dates(person_calculated_dates, date, -100, 0)
        elif event.get_type() == "Marriage" and role == "Witness for":
            update_birth_dates(person_calculated_dates, date, -65, -16)
            update_death_dates(person_calculated_dates, date, 0, 100-16)
        elif event.get_type() == "Baptism" and role in ["Godparent", "Godparent conditional"]:
            update_birth_dates(person_calculated_dates, date, -65, -13)
            update_death_dates(person_calculated_dates, date, 0, 100-13)
        elif event.get_type() in ["Residence", "Census"] and role in ["Primary"]:
            update_birth_dates(person_calculated_dates, date, -100, 0)
            update_death_dates(person_calculated_dates, date, 0, 100)
        elif event.get_type() in ["Mention"] and role in ["Primary", "Recipient"]:
            update_birth_dates(person_calculated_dates, date, -100, 0)
            update_death_dates(person_calculated_dates, date, 0, 100)
        elif event.get_type() in ["Birth", "Death", "Mention"] and role in ["Applicant for (Заявник)"]:
            update_birth_dates(person_calculated_dates, date, -80, -16)
            update_death_dates(person_calculated_dates, date, 0, 100-16)
        elif role == "Primary":
            update_birth_dates(person_calculated_dates, date, -100, 0)
            update_death_dates(person_calculated_dates, date, 0, 100)
        else:
            print(gramps_id, date, role, event.get_type())

    return person_calculated_dates

# Function to calculate events for a person's family
def calcPersonFamily(personFamily, person_calculated_dates):
    family_events = get_events(personFamily.events)
    for event in family_events: 
        date = event.get_date_object()
        date = get_date(date)
        if date is None:
            continue
        if event.get_type() == "Marriage":
            update_birth_dates(person_calculated_dates, date, -70, -16)
            update_death_dates(person_calculated_dates, date, 0, 100-16)
        elif event.get_type() == "Divorce":
            update_birth_dates(person_calculated_dates, date, -70, -16)
            update_death_dates(person_calculated_dates, date, 0, 100-16)
        else:
            print(event.get_type())      

    return person_calculated_dates     

# Function to widen birth and death date ranges
def wideDates(dates, minus, plus):
    if dates['birth']['from'] and dates['birth']['to']:
        dates['birth']['from'] = dates['birth']['from'] + minus
        dates['birth']['to'] = dates['birth']['to'] + plus
    else:
        dates['birth']['from'] = None
        dates['birth']['to'] = None  
    if dates['death']['from'] and dates['death']['to']:
        dates['death']['from'] = dates['death']['from'] + minus
        dates['death']['to'] = dates['death']['to'] + plus
    else:
        dates['death']['from'] = None
        dates['death']['to'] = None 
    return dates 

# Function to combine birth dates from main and correction dates
def combineBirth(main_dates, correction_dates):
    if not correction_dates['birth']['from'] or not correction_dates['birth']['to']:
        return main_dates
    if main_dates['birth']['has_exact_date']:  
        return main_dates 
    if not main_dates['birth']['from']:
        main_dates['birth']['from'] = correction_dates['birth']['from']    
    if not main_dates['birth']['to']:
        main_dates['birth']['to'] = correction_dates['birth']['to'] 
    if main_dates['birth']['from']:
        main_dates['birth']['from'] = get_larger(main_dates['birth']['from'], correction_dates['birth']['from'])
    if main_dates['birth']['to']:
        main_dates['birth']['to'] = get_less(main_dates['birth']['to'], correction_dates['birth']['to'])
    return main_dates

# Function to update birth event with calculated dates
def updateBirthEvent(dates):
    birth_obj = birth.obj
    birth_date = birth_obj.get_date_object()

    birth_from = dates['birth'].get('from')
    birth_to = dates['birth'].get('to')

    if birth_from is None or birth_to is None:
        return

    if birth_from > birth_to:
        return

    if not birth_date.is_empty():
        return

    birth_obj.add_note(auto_update_note.handle)
    
    if birth_from == birth_to:
        birth_date.set(
            quality=Date.QUAL_CALCULATED,
            value=(0, 0, birth_from, False)
        )
    else:
        birth_date.set(
            quality=Date.QUAL_CALCULATED,
            modifier=Date.MOD_RANGE,
            value=(0, 0, birth_from, False, 0, 0, birth_to, False)
        )

    birth_obj.set_date_object(birth_date)
    db.commit_event(birth_obj, trans)
    
# Function to update death event with calculated dates
def updateDeathEvent(dates):
    death_obj = death.obj
    death_date = death_obj.get_date_object()

    death_from = dates['death'].get('from')
    death_to = dates['death'].get('to')

    if death_from is None or death_to is None:
        return

    if death_from > death_to:
        return

    if not death_date.is_empty():
        return

    death_obj.add_note(auto_update_note.handle)
    
    if death_from == death_to:
        death_date.set(
            quality=Date.QUAL_CALCULATED,
            value=(0, 0, death_from, False)
        )
    else:
        death_date.set(
            quality=Date.QUAL_CALCULATED,
            modifier=Date.MOD_RANGE,
            value=(0, 0, death_from, False, 0, 0, death_to, False)
        )

    death_obj.set_date_object(death_date)
    db.commit_event(death_obj, trans)

# Function to synchronize birth dates based on death dates
def syncBirthDates(dates):
    death_from = dates['death']['from']
    death_to = dates['death']['to']
    if dates['birth']['has_exact_date']:  
        return dates
    if death_from:
        dates['birth']['from'] = get_larger(dates["birth"]["from"], death_from - 100)
    if death_to:
        dates['birth']['to'] = get_less(dates["birth"]["to"], death_to)
    return dates

# Function to synchronize death dates based on birth dates
def syncDeathDates(dates):
    birth_from = dates['birth']['from']
    birth_to = dates['birth']['to']
    if dates['death']['has_exact_date']:  
        return dates
    if birth_from:
        dates['death']['from'] = get_larger(dates["death"]["from"], birth_from)
    if birth_to:
        dates['death']['to'] = get_less(dates["death"]["to"], birth_to + 100)
    return dates

[statements]
  
# Calculating events for the main person
dates = calcPersonEvents(self)

# Calculating events for each family member and updating the main person's dates
for personFamily in families:
    dates = calcPersonFamily(personFamily, dates)
  
# Calculating events for each spouse and adjusting birth dates
for spouse in spouses:
    spouseDates = calcPersonEvents(spouse)
    personizedDates = wideDates(spouseDates, -30, 30)
    dates = combineBirth(dates, personizedDates)

# Calculating events for each child and adjusting birth dates based on gender
for child in children:
    childDates = calcPersonEvents(child)
    if gender == "M":
        personizedDates = wideDates(childDates, -70, -18)
    elif gender == "F":
        personizedDates = wideDates(childDates, -55, -16) 
    else:
        personizedDates = wideDates(childDates, -70, -16)           
    dates = combineBirth(dates, personizedDates)

# Calculating events for the father and adjusting birth dates
if father:
    fatherDates = calcPersonEvents(father)
    personizedDates = wideDates(fatherDates, 18, 70)         
    dates = combineBirth(dates, personizedDates)

# Calculating events for the mother and adjusting birth dates
if mother:
    motherDates = calcPersonEvents(mother)
    personizedDates = wideDates(motherDates, 16, 55)         
    dates = combineBirth(dates, personizedDates)    
  
# Synchronizing birth and death dates
dates = syncBirthDates(dates)
dates = syncDeathDates(dates)

# Updating birth and death events with the adjusted dates
# You need uncomment two rows below to update the event in the DB
# updateBirthEvent(dates)
# updateDeathEvent(dates)

[filter]

[expressions]

[scope]
all

[unwind_lists]
True

[commit_changes]
False

[summary_only]
False
1 Like

And interesting script. Thanks.

You might want to examine the code for the Calculate Estimated Dates add-on tool. It flags its additions in a specific way and you might want to adopt that approach. Since the tool has a feature to flush the tree of the Events it created. So if you used the similar flags, you could use their flush. (And that routine has optimization … including temporarily disabling ‘signals’ while doing mass edits to avoid excessive refreshes.)

Yes, I know about the addon Calculate Estimated Dates. Thank you!
What didn’t work for me is that this tool creates some alternative birth and death events instead of updating the existing ones. In reality, this has its pros and cons. However, I did not want to create additional events for myself. Moreover, in the script, I was able to perform a targeted check based on event types and roles, and depending on their values, I was able to calculate birth and death dates more accurately. For example, this fragment:

elif event.get_type() == "Marriage" and role == "Witness for":
    update_birth_dates(person_calculated_dates, date, -65, -16)
    update_death_dates(person_calculated_dates, date, 0, 100-16)
elif event.get_type() == "Baptism" and role in ["Godparent", "Godparent conditional"]:
    update_birth_dates(person_calculated_dates, date, -65, -13)
    update_death_dates(person_calculated_dates, date, 0, 100-13)

It’s unlikely that something similar is implemented in the addon because, for example, a role like “Witness for” is custom-made by me. The roles “Godparent” and “Godparent conditional” also seem to be custom-made. Thus, my script is largely adapted to the specific types and events that are present in my database.

That is why I suggest creating at least such roles in Gramps as “Witness”/“Witness for” for events of the Marriage type and the roles “Godparent”/“Godchild” for the Baptism event (although this might already be present in Gramps, as the project is developing quite intensively). If such native roles already exist or are implemented later, this can be used to perform additional date calculations based on Marriage and Baptism events. I apologize if this already works, and I simply didn’t find it.

1 Like

Ahh. I begin to see.

Then this tool refines dates in existing approximated Events rather than creating missing pivotal/keystone life events.

I’ve often thought that an interactive timeline refining tool would be nice. But the permutations were very complex.

Let’s give an example. Say that death for a person was initially entered as “About 1956”. Gramps has default limits for about as ±50 years. So this date span is evaluated as “Between 1906 and 2006” but loses the higher probability of the death occurring in 1956±2 and even higher of 1956±1. Now when you overlay that he fathered a child born in 1950, was mentioned in sibling/spouse/child/parent obituaries as surviving in 1910, 1930, 1945 and predeceasing in 1958, 1965. That narrows the “Between 1906 and 2006” Death approximation to being definitely “between (1950-9months) and 1958” with a probable date being 1956±2.

(Of course, if the child had been born after 1953, the Fathering a child would have had lower confidence as a limit since that’s when Dr. Jerome K. Sherman documented the 1st artificial insemination with frozen spermatozoa.)

I didn’t use Gramps limits in my calculations. I used dates as strict. For example:
[calculated|estimated|None] [about|None] 1905-01-01
[calculated|estimated|None] [about|None] 1905
And I ignored dates like: range, span, before and after…

I dont understad Gramps limits because the minimal setting value is 1 year. But, what about such example:
Birth date is 1905-01-01, but Baptism is about 1905-01-01. And I undestand that about means 1-2 days, but not 1 year. But If I calculated Birth date and have about 1905, it could be 1904 and 1906. So, different event types can have different limits for about. Thats why I dont trust these limits. I would prefer have ability set limits for about to 0.

But maybe I just dont see so deep and can not use it in a correct way for myself.

It appears that you do understand the limits. You just don’t agree with the range being restricted to years instead of a smaller increment.

I have similar wish that the range could be scalable.

For example:

  • “about 1956” is obviously an approximation range in ‘years’
  • “about Jul 1956” seems more likely to be an approximation range in ‘months’
  • “about 17 Jul 1956” seems more likely to be an approximation range in ‘days’
  • “1970s” seems to be an equivalent to ‘between 1970 and 1979’
2 Likes

yeah, looks like you defined the problem more exactly than me )))

One of the issues is the English language and semantics

And these are the basics I use because I am still not sure how dates
were supposed to be used and is another example of an incredibly steep
learning curve which puts people off GRAMPS.

So range “between” “and” is exactly the same as using using “from” “to”
which for some reason GRAMPS takes as a span

So range
between 01/01/2020 and 09/10/2024
or from 01/01/2020 to 09/10/2024
both to me are the same

2024 is inherently a range between 01/01/2024 and 31/12/2024
May 2024 is inherently a range between 01/05/2024 and 31/05/2024

The use of the word about in context is a span which should be of the
form “date” and “duration”
so 1756 +/- 5 years which actually would convert to the range
from 01/01/1751 to 31/12/1761 thus proving span is irrelevant and why I
deleted it from DateHandler

the words “before” “after” give different context
so before 1756 is any date prior 01/01/1756
after 1756 is any date post 31/12/1756
and should have some appropriate limit (or duration) would then convert
it to a range.

I never use any form of calculated ages or dates to me they are
irrelevant I either have the date or I do not.

I also have changed Datehandler to aid presentation in Graph View
so that
< = before

= after
~ = circa which effectively is a guess around that +/- decade (or
generation)
<> = range “between” “and” where I have the dates in Day/Month/Year
format and no other

and that is all

The only age I use is in the form
died in his/her 82nd year so from day 1 after the 81st birthday until
the 82 birthday which GRAMPS cannot handle

I expect everyone uses their own set of guidelines for dating, this is
just what works for me

phil

1 Like

These are not the same.

From and To indicates an Event that is true continuously during that timeframe. Such as they lived at 123 Main street. They moved in at the “from” date and moved out at the “to” date.

A “between” means the Event happened at some point within the specified period.

2 Likes

I always use them because I have a lot of repeating full names. And calculated dates help me to understand who of the people is what I need at the moment

By the way, I think you are not the first one to not use date calculations. Maybe I am the first one to use them. )))
Here’s how, in my opinion, this functionality could look like to meet the needs of both users who do not use date calculations and users who do it in Gramps. It could be a built-in date calculation mechanism could be implemented in the form of a separate table in the database. These dates are calculated automatically and are always up-to-date. In the interface, the user sees them as placeholders, in a lighter color in all lists and in the person’s card. The user can configure several things:

  • Enable/disable their display in the interface
  • Enable/disable their export in various reports
  • Disable the automatic calculation function entirely (in case of weak PCs where this could be critical)

Thus, we have several advantages:

  1. No need to do manual recalculations of these dates.
  2. For convenience and ability to research calculated date ranges are visible.
  3. The main database remains clean without any calculated entries.
  4. It is secure due to the separate table - main data can not be harmed.

From what I’ve seen regarding performance, dynamic automatic recalculation of data for one person does not affect performance in any way, it’s an instant process, as one person actually doesn’t require many calculations.

That in my view is GRAMPS semantics nothing to do with dates
There are only 3 types of date

Accurate dates “01 Jan 2020”
Implicit Ranges “May 2020” or “2020”
Explicit Ranges “between 01 Jan 2019 and 31 Dec 2020”

In a Tree of 15000 people I have none where I use Residency in the form
you describe as I have no evidence to support people going to or leaving
from a specific location.
phil

I don’t use calculated dates because having no date tells me I have not found a source for that event. However I may add an “about” date and country to allow for a very broad categorization.
It all comes down to the workflow that works for you. In England there are many Baptism records and depending on the time period, few Birth records. When I only have the Baptism date, I create a Birth event using the “before” Baptism date. Same for a Death date when I only have the Burial date. I don’t create people records unless they are connected to the tree. That means I don’t have a “witness” person unless they are a relative.

1 Like

do you have enough documents to build the tree? A lot of documents are not saved in my region, so, I must save witnesses and godparents assuming that very often witnesses and godparents were relatives. This is one of possible ways how research and then define women’s maiden names.

Just interesting, looks like each of Gramps users uses diametrally opposite ways for own genealogy :upside_down_face:

Since most of my research is from the UK, Canada, USA, Australia and New Zealand, the answer is yes. However I have a branch that are Dutch and I have hit a roadblock there because I don’t understand the language/culture well enough.

1 Like

I used to record witnesses until I started on a period where a lot of
“Events”, Baptism, Marriage were occurring at Manchester Cathedral which
was renowned as a place where you could get married and or baptised no
questions asked just “give us the money” it was also cheaper than
anywhere else. It was described as a conveyor belt operation hundreds
were done in a day witnesses were paid individuals who did it regularly,
clerks wrote out the certificates and some cases signed them (better
described as forged) as the priest was too busy marrying the the next lot.

So I stopped because I was accumulating rubbish which took some clearing
out.
But if it works for you all well and good
phil

1 Like