NAME

DateTime::Format::Genealogy - Create a DateTime object from a genealogy date string

VERSION

Version 0.13

SYNOPSIS

DateTime::Format::Genealogy is a Perl module designed to parse genealogy-style date strings (primarily GEDCOM format) and convert them into DateTime objects. It wraps Genealogy::Gedcom::Date and DateTime::Format::Natural, adds GEDCOM calendar-escape handling, and accepts common non-standard month names found in exported genealogical trees.

use DateTime::Format::Genealogy;
my $dtg = DateTime::Format::Genealogy->new();
my $dt  = $dtg->parse_datetime('25 Dec 2022');
print $dt->dmy;  # 25-12-2022

SUBROUTINES/METHODS

new

Creates or clones a DateTime::Format::Genealogy object.

EXAMPLE

# Bare construction
my $dtg = DateTime::Format::Genealogy->new();

# Construction with flags stored on the object
my $dtg_quiet = DateTime::Format::Genealogy->new(quiet => 1, strict => 1);

# Clone with an override (inherits quiet => 1, overrides strict)
my $clone = $dtg_quiet->new(strict => 0);

API SPECIFICATION

# Input (hash or hashref, all keys optional):
{
    quiet  => $bool,  # suppress carp in internal helpers
    strict => $bool,  # enforce 3-letter GEDCOM month abbreviations
    # Any extra keys from Object::Configure are also accepted.
}

# Output: blessed DateTime::Format::Genealogy object

MESSAGES

This method does not emit any diagnostics directly.

PSEUDOCODE

FUNCTION new(class, *args):
  params = get_params(undef, args)
  IF class is not defined:
    class = __PACKAGE__
  ELSE IF class is already an object (blessed):
    RETURN bless( merge(class.attrs, params), ref(class) )
  params = configure(class, params)   # merge any config-file settings
  RETURN bless(params, class)
END FUNCTION

parse_datetime

Parses a genealogy-style date string and returns a DateTime object.

Recognises GEDCOM calendar escapes (@#DJULIAN@, @#DHEBREW@, @#DFRENCH R@) and converts them via the appropriate calendar module when available.

Can be called as a class method, an object method, or a bare function.

Returns:

Mandatory argument:

Optional arguments (may be set at construction time and/or overridden per-call; per-call values take precedence):

EXAMPLE

my $dtg = DateTime::Format::Genealogy->new();

# Simple exact date
my $dt = $dtg->parse_datetime('25 Dec 2022');
print $dt->dmy;  # 25-12-2022

# Date range (list context)
my ($start, $end) = $dtg->parse_datetime('bet 1 Sep 1939 and 2 Sep 1945');

# GEDCOM calendar escape
my $julian = $dtg->parse_datetime('@#DJULIAN@ 15 Mar 1620');

# Class-method form (no constructor required)
my $dt2 = DateTime::Format::Genealogy->parse_datetime('1 Jan 2000');

# Long month name (non-strict only)
my $dt3 = $dtg->parse_datetime('12 June 2020');

# French month variant (non-strict only)
my $dt4 = $dtg->parse_datetime('21 Mai 1681');

API SPECIFICATION

# Input (hash or hashref):
{
    date   => $string,         # required (non-ref, non-empty)
    quiet  => $bool,           # optional; falls back to $self->{'quiet'}
    strict => $bool,           # optional; falls back to $self->{'strict'}
}

# Return values:
#   DateTime                   exact parseable date
#   (DateTime, DateTime)       date range (list context only)
#   undef / ()                 unparseable, approximate, or year-only

MESSAGES

PSEUDOCODE

FUNCTION parse_datetime(self, *args):
  -- Dispatch class/function/hash-invocant calls to an object instance
  IF self is not a reference:
    RETURN new()->parse_datetime(args or self)
  IF ref(self) == 'HASH':
    RETURN new()->parse_datetime(self)

  ABORT unless args non-empty
  params = get_params('date', args)
  ABORT on unknown keys (validate_strict)

  date   = params.date
  quiet  = params.quiet  // self.quiet
  strict = params.strict // self.strict

  ABORT unless date is defined, non-empty, and not a reference

  -- Strip GEDCOM calendar escape if present
  IF date =~ s/^@#D([A-Z ]+?)@\s*//: calendar_type = 'D' + uc(match)

  -- Reject approximate/relative dates
  IF date =~ /^(bef|aft|abt)\s/i: CARP and RETURN undef

  -- Reject calendar impossibilities
  IF date =~ /^31\s+Nov/: CARP and RETURN undef

  -- Rewrite dash-separated ranges and ISO dates
  IF date =~ /X - Y/:
    IF date =~ /YYYY-MM-DD/: REFORMAT to "DD Mon YYYY" (carp)
    ELSE: REFORMAT to "bet X and Y" (carp)

  -- Dispatch ranges to recursive calls
  IF date =~ /^bet X and Y/i:
    RETURN (parse_datetime(X), parse_datetime(Y)) IF wantarray
    RETURN undef

  IF !strict AND date =~ /^from X to Y/i:
    RETURN (parse_datetime(X), parse_datetime(Y)) IF wantarray
    RETURN undef

  -- Normalise non-standard month names (non-strict mode only)
  IF !strict:
    IF date =~ DD + Aout + YYYY (French non-ASCII August):
      REWRITE month to 'Aug'
    ELSE IF date =~ /DD LONG_OR_VARIANT YYYY/:
      lookup = MONTH_ALIAS{ucfirst(lc(month))}
      IF lookup: REWRITE month to lookup
      ELSE IF month is more than 3 letters: CARP and RETURN undef
      -- 3-letter unknown months fall through unchanged to the parser

  -- Parse with Genealogy::Gedcom::Date (cached) then DateTime::Format::Natural
  IF date starts with digit:
    d = _date_parser_cached(date)
    IF d defined:
      RETURN undef if date ends with year-only (< AD100 guard)
      rc = DateTime::Format::Natural->parse_datetime(d.canonical)
      IF calendar_type != DGREGORIAN:
        rc = _convert_calendar(rc, calendar_type, quiet)
      RETURN rc

  -- Fallback: try DateTime::Format::Natural directly on the raw string
  IF date not ~= /^(Abt|ca?)/i AND date =~ /^[\w\s,]+$/:
    rc = DateTime::Format::Natural->parse_datetime(date)
    IF rc AND success: RETURN rc
    ELSE: CARP error

  RETURN undef
END FUNCTION

LIMITATIONS

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

BUGS

Please report any bugs or feature requests to the author. This module is provided as-is without any warranty.

SEE ALSO

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc DateTime::Format::Genealogy

FORMAL SPECIFICATION

new

State S ::= [ attrs : Name -> Value ]

new(class, params) = bless(params U configure(class))   when !blessed(class)
new(obj,   params) = bless(obj.attrs (+) params)         when  blessed(obj)

where (+) denotes right-biased union (params values take precedence).

parse_datetime

Let D be the set of genealogical date strings and DT the co-domain of DateTime objects.

parse_datetime : D -> DT | bot | (DT x DT)

YearOnly ::= { s in D | s ~ /^\d{3,4}$/ }
Approx   ::= { s in D | s ~ /^(bef|aft|abt)\s/i }
Ranges   ::= { s in D | s ~ /^bet\s.+\sand\s.+/i
                       | s ~ /^from\s.+\sto\s.+/i }

Pre:  date != bot

Post:
  date in YearOnly              => result = bot
  date in Approx                => result = bot  (carp unless quiet)
  date in Ranges /\ wantarray   => result in DT x DT
  date in Ranges /\ !wantarray  => result = bot
  date in Parseable \
    (YearOnly u Approx u Ranges) => result in DT
  date not in Parseable         => result = bot  (carp unless quiet)

LICENSE AND COPYRIGHT

Copyright 2018-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.