package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// lintCmd represents the lint command
var lintCmd = &cobra.Command{
Use: "lint",
Short: "Analyze a GEDCOM file",
Long: `Analyze a GEDCOM file for errors, duplication, and common errors.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("lint called")
// TODO: Perform sort of linting against the GEDCOM.
},
}
func init() {
rootCmd.AddCommand(lintCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// lintCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// lintCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package cmd
import (
"fmt"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
// Used for flags.
cfgFile string
userLicense string
gedcomFile string
rootCmd = &cobra.Command{
Use: "gedcom",
Short: "A CLI to operate against a GEDCOM file",
Long: `Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
)
var version = "1.0.0"
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
// rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
// rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
rootCmd.PersistentFlags().StringVarP(&gedcomFile, "filename", "f", "", "path to a gedcom file")
// rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
// viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
// viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
// viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
// viper.SetDefault("license", "apache")
// rootCmd.AddCommand(addCmd)
// rootCmd.AddCommand(initCmd)
}
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
cobra.CheckErr(err)
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
package cmd
import (
"fmt"
"strings"
"github.com/adamisrael/gedcom"
"github.com/adamisrael/gedcom/types"
"github.com/spf13/cobra"
)
var (
location bool
caseSensitive bool = false
)
// searchCmd represents the search command
var searchCmd = &cobra.Command{
Use: "search",
Short: "Search the contents of a GEDCOM file",
Long: `A simple, lightweight interface to searching the contents of a GEDCOM file.
For more advanced searching, use gquery.`,
Run: func(cmd *cobra.Command, args []string) {
g, err := gedcom.OpenGedcom(gedcomFile)
if g == nil || err != nil {
fmt.Printf("Invalid GEDCOM file: %s\n", err)
return
}
if location {
fmt.Printf("Search location\n")
}
// Do a simple search of the GEDCOM, searching Name and optionally location(s)
if len(args) > 0 {
var found = make(map[*types.Individual]int)
if location {
found = findByLocation(args, g.Individual, caseSensitive)
} else {
found = findByName(args, g.Individual, caseSensitive)
}
for i := range found {
if found[i] >= len(args) {
fmt.Printf("[%d] %s\n", found[i], i.Name[0].Name)
}
}
}
},
}
// findByName resturns individuals who match the search criteria
func findByName(needles []string, individuals []*types.Individual, caseSensitive bool) map[*types.Individual]int {
var found = make(map[*types.Individual]int)
for _, i := range individuals {
var name = i.Name[0].Name
if caseSensitive == false {
name = strings.ToLower(name)
}
for _, needle := range needles {
if caseSensitive == false {
needle = strings.ToLower(needle)
}
if strings.Contains(name, needle) {
found[i] += 1
}
}
}
return found
}
// findByLocation finds individuals who match the location
func findByLocation(needles []string, individuals []*types.Individual, caseSensitive bool) map[*types.Individual]int {
var found = make(map[*types.Individual]int)
// There's lots of normalization issues with the data, so we're just doing a token match
for _, i := range individuals {
for _, e := range i.Event {
for _, needle := range needles {
if caseSensitive == false {
needle = strings.ToLower(needle)
}
// We only want to mark found once per needle
if strings.Contains(matchCase(e.Address.Full, caseSensitive), needle) {
fmt.Printf(".")
found[i] += 1
} else if strings.Contains(matchCase(e.Place.Name, caseSensitive), needle) {
fmt.Printf(".")
found[i] += 1
} else if strings.Contains(matchCase(e.Address.City, caseSensitive), needle) {
found[i] += 1
}
}
}
}
return found
}
func matchCase(s string, caseSensitive bool) string {
if caseSensitive {
return s
} else {
return strings.ToLower(s)
}
}
func init() {
rootCmd.AddCommand(searchCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// searchCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// searchCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
searchCmd.Flags().BoolVar(&location, "location", false, "Include locations in search")
searchCmd.Flags().BoolVar(&caseSensitive, "case-sensitive", false, "Perform a case-sensitive search")
}
package cmd
import (
"fmt"
"github.com/adamisrael/gedcom"
"github.com/spf13/cobra"
)
// statsCmd represents the stats command
var statsCmd = &cobra.Command{
Use: "stats",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
g, err := gedcom.OpenGedcom(gedcomFile)
if g == nil || err != nil {
fmt.Printf("Invalid GEDCOM file: %s\n", err)
return
}
// Display statistics about this GEDCOM
fmt.Println("GEDCOM Statistics:")
fmt.Printf("%d individuals\n", len(g.Individual))
fmt.Printf("%d families\n", len(g.Family))
fmt.Printf("%d sources\n", len(g.Source))
},
}
func init() {
rootCmd.AddCommand(statsCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// statsCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// statsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package cmd
import (
"fmt"
"github.com/adamisrael/gedcom"
"github.com/spf13/cobra"
)
// lintCmd represents the lint command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Display version",
Long: `Display the version of gedcom`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Gedcom CLI Version %s\n", version)
g, err := gedcom.OpenGedcom(gedcomFile)
if g != nil || err == nil {
fmt.Printf("GEDCOM version: %s\n", g.Header.Version)
fmt.Printf("GEDCOM CharSet: %s\n", g.Header.CharacterSet.Name)
// fmt.Printf("GEDCOM ID: %s\n", g.Header.ID)
return
}
},
}
func init() {
rootCmd.AddCommand(versionCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// lintCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// lintCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package main
import "github.com/adamisrael/gedcom/cmd/gedcom/cmd"
func main() {
cmd.Execute()
}
package date
import (
"time"
"github.com/adamisrael/gedcom/types"
)
// isSameDay checks if the event occured on today's month/day
func IsSameDay(event types.Event) bool {
if len(event.Date) > 0 {
t, err := Parse(event.Date)
if err == nil {
_, month, day := DateDiff(t, time.Now())
if month == 0 && day == 0 {
return true
}
}
}
return false
}
func DateDiff(a, b time.Time) (year, month, day int) {
if a.Location() != b.Location() {
b = b.In(a.Location())
}
if a.After(b) {
a, b = b, a
}
y1, M1, d1 := a.Date()
y2, M2, d2 := b.Date()
year = int(y2 - y1)
month = int(M2 - M1)
day = int(d2 - d1)
// Normalize negative values
if day < 0 {
// days in month:
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
day += 32 - t.Day()
month--
}
if month < 0 {
month += 12
year--
}
return
}
package date
import (
"time"
"github.com/araddon/dateparse"
)
/*
Dates in GEDCOM can be fuzzy, i.e., all of these are valid
Between 4 Apr 1935 and 9 Apr 1935
Btw. 4 April 1935 and 9 April 1935
Between 4 April 1935 and 9 April 1935
Abt. April 1935
About Apr 1935
After Apr. 4 1935
4 April 1935
4 Apr 1935
4 Apr. 1935
April 4, 1935
*/
// Parse will attempt to parse a date string and return
func Parse(date string) (time.Time, error) {
var t time.Time
var err error
// For now, try to parse it with dateparse first
t, err = dateparse.ParseLocal(date)
if err != nil {
// attempt to parse it manually
}
return t, err
}
package gedcom
import (
"bufio"
"errors"
"os"
"github.com/adamisrael/gedcom/parser"
"github.com/adamisrael/gedcom/types"
)
// How do I export types from types.* here as gedcom.Gedcom?
// OpenGedcom will open a filename, if it exists, and parse it as a GEDCOM
func OpenGedcom(filename string) (*types.Gedcom, error) {
if _, err := os.Stat(filename); err == nil {
f, err := os.Open(filename)
check(err)
defer f.Close()
p := parser.NewParser(bufio.NewReader(f))
g, err := p.Parse()
check(err)
return g, err
}
return nil, errors.New("invalid GEDCOM file")
}
// Gedcom is the main entrypoint
func Gedcom(gedcomFile string) *types.Gedcom {
f, err := os.Open(gedcomFile)
check(err)
defer f.Close()
p := parser.NewParser(bufio.NewReader(f))
g, err := p.Parse()
check(err)
return g
}
func check(e error) {
if e != nil {
panic(e)
}
}
package parser
/**
* GEDCOM grammar rules for gedcom_line(s)
* Source: http://www.phpgedview.net/ged551-5.pdf
- Long values can be broken into shorter GEDCOM lines by using a
subordinate CONC or CONT tag. The CONC tag assumes that the accompanying
subordinate value is concatenated to the previous line value without saving
the carriage return prior to the line terminator. If a concatenated line is
broken at a space, then the space must be carried over to the next line.
The CONT assumes that the subordinate line value is concatenated to the
previous line, after inserting a carriage return.
- The beginning of a new logical record is designated by a line whose level number is 0 (zero).
- Level numbers must be between 0 to 99 and must not contain leading zeroes, for example, level one must be 1, not 01.
- Each new level number must be no higher than the previous line plus 1.
- All GEDCOM lines have either a value or a pointer unless the line
contains subordinate GEDCOM lines. The presence of a level number and a tag
alone should not be used to assert data (i.e. 1 FLAG Y not just 1 FLAG to
imply that the flag is set).
- Logical GEDCOM record sizes should be constrained so that they will fit
in a memory buffer of less than 32K. GEDCOM files with records sizes
greater than 32K run the risk of not being able to be loaded in some
programs. Use of pointers to records, particularly NOTE records, should
ensure that this limit will be sufficient.
- Any length constraints are given in characters, not bytes. When wide
characters (characters wider than 8 bits) are used, byte buffer lengths
should be adjusted accordingly.
- The cross-reference ID has a maximum of 22 characters, including the
enclosing ‘at’ signs (@), and it must be unique within the GEDCOM
transmission.
- Pointers to records imply that the record pointed to does actually exists
within the transmission. Future pointer structures may allow pointing to
records within a public accessible database as an alternative.
- The length of the GEDCOM TAG is a maximum of 31 characters, with the
first 15 characters being unique.
- The total length of a GEDCOM line, including level number,
cross-reference number, tag, value, delimiters, and terminator, must not
exceed 255 (wide) characters.
- Leading white space (tabs, spaces, and extra line terminators) preceding
a GEDCOM line should be ignored by the reading system. Systems generating
GEDCOM should not place any white space in front of the GEDCOM line.
*/
import (
"io"
"strings"
"github.com/adamisrael/gedcom/types"
)
// Parser represents a parser.
type Parser struct {
s *Scanner
header types.Header
refs map[string]interface{}
// r io.Reader
// buf struct {
// // tok Token // last read token
// lit string // last read literal
// n int // buffer size (max=1)
// }
parsers []parser
// Individual []types.Individual
}
type parser func(level int, tag string, value string, xref string) error
func (p *Parser) pushParser(P parser) {
p.parsers = append(p.parsers, P)
}
func (p *Parser) popParser(level int, tag string, value string, xref string) error {
n := len(p.parsers) - 1
if n < 1 {
panic("MASSIVE ERROR") // TODO
}
p.parsers = p.parsers[0:n]
return p.parsers[len(p.parsers)-1](level, tag, value, xref)
}
// NewParser returns a new instance of Parser.
func NewParser(r io.Reader) *Parser {
return &Parser{s: NewScanner(r)}
}
func (p *Parser) Parse() (*types.Gedcom, error) {
g := &types.Gedcom{
Header: types.Header{ID: "test"},
}
p.header = types.Header{
Version: "1.1",
}
p.refs = make(map[string]interface{})
p.parsers = []parser{makeRootParser(p, g)}
p.Scan(g)
g.Header = p.header
return g, nil
}
func (p *Parser) Scan(g *types.Gedcom) {
s := &Scanner{}
pos := 0
for {
line, err := p.s.r.ReadString('\n')
if err != nil {
// TODO
}
// fmt.Print(line)
s.Reset()
offset, err := s.Scan(line)
pos += offset
if err != nil {
if err != io.EOF {
println(err.Error())
return
}
break
}
p.parsers[len(p.parsers)-1](s.level, string(s.tag), string(s.value), string(s.xref))
}
}
func makeRootParser(p *Parser, g *types.Gedcom) parser {
return func(level int, tag string, value string, xref string) error {
// println(level, tag, value, xref)
if level == 0 {
switch tag {
case "HEAD":
obj := p.head(xref)
p.pushParser(makeHeaderParser(p, obj, level))
case "INDI":
obj := p.individual(xref)
g.Individual = append(g.Individual, obj)
p.pushParser(makeIndividualParser(p, obj, level))
case "SUBM":
g.Submitter = append(g.Submitter, &types.Submitter{})
case "FAM":
obj := p.family(xref)
g.Family = append(g.Family, obj)
p.pushParser(makeFamilyParser(p, obj, level))
case "SOUR":
obj := p.source(xref)
g.Source = append(g.Source, obj)
//d.pushParser(makeSourceParser(d, s, level))
}
}
return nil
}
}
func (p *Parser) head(xref string) *types.Header {
return &p.header
}
func makeHeaderParser(p *Parser, h *types.Header, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
if level == 1 {
switch tag {
case "CHAR":
h.CharacterSet.Name = value
case "GEDC":
p.pushParser(makeVersionParser(p, h, level))
}
}
return nil
}
}
func (p *Parser) individual(xref string) *types.Individual {
if xref == "" {
return &types.Individual{}
}
ref, found := p.refs[xref].(*types.Individual)
if !found {
rec := &types.Individual{Xref: xref}
p.refs[rec.Xref] = rec
return rec
}
return ref
}
func makeIndividualParser(p *Parser, i *types.Individual, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "FAMC":
family := p.family(stripXref(value))
f := &types.FamilyLink{Family: family}
i.Parents = append(i.Parents, f)
p.pushParser(makeFamilyLinkParser(p, f, level))
case "FAMS":
family := p.family(stripXref(value))
f := &types.FamilyLink{Family: family}
i.Family = append(i.Family, f)
p.pushParser(makeFamilyLinkParser(p, f, level))
case "BIRT", "CHR", "DEAT", "BURI", "CREM", "ADOP", "BAPM", "BARM", "BASM", "BLES", "CHRA", "CONF", "FCOM", "ORDN", "NATU", "EMIG", "IMMI", "CENS", "PROB", "WILL", "GRAD", "RETI", "EVEN":
e := &types.Event{Tag: tag, Value: value}
i.Event = append(i.Event, e)
p.pushParser(makeEventParser(p, e, level))
case "CAST", "DSCR", "EDUC", "IDNO", "NATI", "NCHI", "NMR", "OCCU", "PROP", "RELI", "RESI", "SSN", "TITL", "FACT":
e := &types.Event{Tag: tag, Value: value}
i.Attribute = append(i.Attribute, e)
p.pushParser(makeEventParser(p, e, level))
case "NAME":
// The Gedcom stores the name as "First Middle /Last/". Store the
// original, but parse out the given and surname, too.
var given, surname, suffix string
/*
* Given the following examples:
* a) "Adam Michael /Israel/"
* b) "Adam Michael"
* c) "/Israel/"
* d) "Adam Michael /Israel/ Sr"
*
* a, c, and d will return a three element slice:
* 0 holding the given name(s)
* 1 holding the surname
* 2 holding the suffix.
* b will return a single element slice with just the given name.
*
*/
names := strings.Split(value, "/")
given = strings.TrimSpace(names[0])
if len(names) == 3 {
surname = names[1]
suffix = names[2]
}
n := &types.Name{
Name: value,
Given: given,
Surname: surname,
Suffix: suffix}
i.Name = append(i.Name, n)
p.pushParser(makeNameParser(p, n, level))
case "SEX":
i.Sex = value
}
return nil
}
}
func makeNameParser(p *Parser, n *types.Name, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "SOUR":
c := &types.Citation{Source: p.source(stripXref(value))}
n.Citation = append(n.Citation, c)
p.pushParser(makeCitationParser(p, c, level))
case "NOTE":
r := &types.Note{Note: value}
n.Note = append(n.Note, r)
p.pushParser(makeNoteParser(p, r, level))
}
return nil
}
}
func makeSourceParser(p *Parser, s *types.Source, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "TITL":
s.Title = value
p.pushParser(makeTextParser(p, &s.Title, level))
case "NOTE":
r := &types.Note{Note: value}
s.Note = append(s.Note, r)
p.pushParser(makeNoteParser(p, r, level))
}
return nil
}
}
func makeVersionParser(p *Parser, h *types.Header, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "VERS":
h.Version = value
}
return nil
}
}
func makeCitationParser(p *Parser, c *types.Citation, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "PAGE":
c.Page = value
case "QUAY":
c.Quay = value
case "NOTE":
r := &types.Note{Note: value}
c.Note = append(c.Note, r)
p.pushParser(makeNoteParser(p, r, level))
case "DATA":
p.pushParser(makeDataParser(p, &c.Data, level))
}
return nil
}
}
func makeNoteParser(p *Parser, n *types.Note, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "CONT":
n.Note = n.Note + "\n" + value
case "CONC":
n.Note = n.Note + value
case "SOUR":
c := &types.Citation{Source: p.source(stripXref(value))}
n.Citation = append(n.Citation, c)
p.pushParser(makeCitationParser(p, c, level))
}
return nil
}
}
func makeTextParser(p *Parser, s *string, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "CONT":
*s = *s + "\n" + value
case "CONC":
*s = *s + value
}
return nil
}
}
func makeDataParser(p *Parser, r *types.Data, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "DATE":
r.Date = value
case "TEXT":
r.Text = append(r.Text, value)
p.pushParser(makeTextParser(p, &r.Text[len(r.Text)-1], level))
}
return nil
}
}
func makePlaceParser(p *Parser, pl *types.Place, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "SOUR":
c := &types.Citation{Source: p.source(stripXref(value))}
pl.Citation = append(pl.Citation, c)
p.pushParser(makeCitationParser(p, c, level))
case "NOTE":
r := &types.Note{Note: value}
pl.Note = append(pl.Note, r)
p.pushParser(makeNoteParser(p, r, level))
}
return nil
}
}
func makeFamilyLinkParser(p *Parser, f *types.FamilyLink, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "PEDI":
f.Type = value
case "NOTE":
r := &types.Note{Note: value}
f.Note = append(f.Note, r)
p.pushParser(makeNoteParser(p, r, level))
}
return nil
}
}
func makeFamilyParser(p *Parser, f *types.Family, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "HUSB":
f.Husband = p.individual(stripXref(value))
case "WIFE":
f.Wife = p.individual(stripXref(value))
case "CHIL":
f.Child = append(f.Child, p.individual(stripXref(value)))
case "ANUL", "CENS", "DIV", "DIVF", "ENGA", "MARR", "MARB", "MARC", "MARL", "MARS", "EVEN":
e := &types.Event{Tag: tag, Value: value}
f.Event = append(f.Event, e)
p.pushParser(makeEventParser(p, e, level))
}
return nil
}
}
func makeAddressParser(p *Parser, a *types.Address, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "CONT":
a.Full = a.Full + "\n" + value
case "ADR1":
a.Line1 = value
case "ADR2":
a.Line2 = value
case "CITY":
a.City = value
case "STAE":
a.State = value
case "POST":
a.PostalCode = value
case "CTRY":
a.Country = value
case "PHON":
a.Phone = append(a.Phone, value)
case "EMAIL":
a.Email = append(a.Email, value)
case "FAX":
a.Fax = append(a.Fax, value)
case "WWW":
a.WWW = append(a.WWW, value)
}
return nil
}
}
func makeEventParser(p *Parser, e *types.Event, minLevel int) parser {
return func(level int, tag string, value string, xref string) error {
if level <= minLevel {
return p.popParser(level, tag, value, xref)
}
switch tag {
case "TYPE":
e.Type = value
case "DATE":
e.Date = value
case "PLAC":
e.Place.Name = value
p.pushParser(makePlaceParser(p, &e.Place, level))
case "ADDR":
e.Address.Full = value
p.pushParser(makeAddressParser(p, &e.Address, level))
case "SOUR":
c := &types.Citation{Source: p.source(stripXref(value))}
e.Citation = append(e.Citation, c)
p.pushParser(makeCitationParser(p, c, level))
case "NOTE":
r := &types.Note{Note: value}
e.Note = append(e.Note, r)
p.pushParser(makeNoteParser(p, r, level))
}
return nil
}
}
func (p *Parser) source(xref string) *types.Source {
if xref == "" {
return &types.Source{}
}
ref, found := p.refs[xref].(*types.Source)
if !found {
rec := &types.Source{Xref: xref}
p.refs[rec.Xref] = rec
return rec
}
return ref
}
func (p *Parser) family(xref string) *types.Family {
if xref == "" {
return &types.Family{}
}
ref, found := p.refs[xref].(*types.Family)
if !found {
rec := &types.Family{Xref: xref}
p.refs[rec.Xref] = rec
return rec
}
return ref
}
func stripXref(value string) string {
return strings.Trim(value, "@")
}
package parser
import (
"bufio"
// "bytes"
"fmt"
"io"
"strconv"
// "strings"
)
// Scanner represents a lexical scanner.
type Scanner struct {
r *bufio.Reader
parseState int
tokenStart int
level int
tag string
value string
xref string
}
func (s *Scanner) Reset() {
s.parseState = STATE_BEGIN
s.tokenStart = 0
s.level = 0
// s.xref = make([]byte, 0)
// s.tag = make([]byte, 0)
// s.value = make([]byte, 0)
s.xref = ""
s.tag = ""
s.value = ""
}
// NewScanner returns a new instance of Scanner.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{r: bufio.NewReader(r)}
}
// Scan returns the next token and literal value.
// func (s *Scanner) Scan() (tok Token, lit string) {
// Scan returns the next tag
func (s *Scanner) Scan(data string) (offset int, err error) {
for i, c := range data {
switch s.parseState {
case STATE_BEGIN:
switch {
case c >= '0' && c <= '9':
s.tokenStart = i
s.parseState = STATE_LEVEL
// fmt.Printf("Found level %c\n", c)
case isWhitespace(c):
continue
default:
s.parseState = STATE_ERROR
err = fmt.Errorf("Found non-whitespace before level: %q", data)
return
}
case STATE_LEVEL:
switch {
case c >= '0' && c <= '9':
continue
case c == ' ':
parsedLevel, perr := strconv.ParseInt(string(data[s.tokenStart:i]), 10, 64)
if perr != nil {
err = perr
return
}
s.level = int(parsedLevel)
s.parseState = SEEK_TAG_OR_XREF
default:
s.parseState = STATE_ERROR
err = fmt.Errorf("Level contained non-numerics")
return
}
case SEEK_TAG:
switch {
case isAlphaNumeric(c):
s.tokenStart = i
s.parseState = STATE_TAG
case c == ' ':
continue
default:
s.parseState = STATE_ERROR
err = fmt.Errorf("Tag \"%s\" contained non-alphanumeric", string(data[s.tokenStart:i]))
return
}
case SEEK_TAG_OR_XREF:
switch {
case isAlphaNumeric(c):
s.tokenStart = i
s.parseState = STATE_TAG
case c == '@':
s.tokenStart = i
s.parseState = STATE_XREF
case c == ' ':
continue
default:
s.parseState = STATE_ERROR
err = fmt.Errorf("Xref \"%s\" contained non-alphanumeric", string(data[s.tokenStart:i]))
return
}
case STATE_TAG:
switch {
case isAlphaNumeric(c):
continue
case c == '\n' || c == '\r':
s.tag = data[s.tokenStart:i]
s.parseState = STATE_END
offset = i
return
case c == ' ':
s.tag = data[s.tokenStart:i]
s.parseState = SEEK_VALUE
default:
s.parseState = STATE_ERROR
err = fmt.Errorf("Tag contained non-alphanumeric")
return
}
case STATE_XREF:
switch {
case isAlphaNumeric(c) || c == '@':
continue
case c == ' ':
s.xref = data[s.tokenStart+1 : i-1]
s.parseState = SEEK_TAG
default:
s.parseState = STATE_ERROR
err = fmt.Errorf("Xref contained non-alphanumeric \"%c\"", c)
return
}
case SEEK_VALUE:
switch {
case c == '\n' || c == '\r':
s.parseState = STATE_END
offset = i
return
case c == ' ':
continue
default:
s.tokenStart = i
s.parseState = STATE_VALUE
}
case STATE_VALUE:
switch {
case c == '\n' || c == '\r':
s.value = data[s.tokenStart:i]
s.parseState = STATE_END
offset = i
return
default:
continue
}
}
}
return 0, io.EOF
}
// read reads the next rune from the buffered reader.
// Returns the rune(0) if an error occurs (or io.EOF is returned).
// func (s *Scanner) read() rune {
// ch, _, err := s.r.ReadRune()
// if err != nil {
// return eof
// }
// return ch
// }
// unread places the previously read rune back on the reader.
// func (s *Scanner) unread() { _ = s.r.UnreadRune() }
// isWhitespace returns true if the rune is a space, tab, or newline.
func isWhitespace(ch rune) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' }
// isLetter returns true if the rune is a letter.
func isAlpha(ch rune) bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' }
// isDigit returns true if the rune is a digit.
func isNumeric(ch rune) bool { return (ch >= '0' && ch <= '9') }
func isAlphaNumeric(ch rune) bool { return isAlpha(ch) || isNumeric(ch) }
// eof represents a marker rune for the end of the reader.
var eof = rune(0)
package relationship
import (
"fmt"
"github.com/adamisrael/gedcom/types"
"github.com/dustin/go-humanize"
)
/*
Handle all calculations to determine the relationship between Individuals
in a tree.
TODO: Support half relations
*/
// Relationship represents the genealogical relationship
// between two individuals, used for calculations
type Relationship struct {
Xref string
Person types.Individual
Generations int
Removed int
Relationship string
}
func CalculateRelationship(home types.Individual, target types.Individual) string {
var relationship = ""
// iterate through each generation to find a common ancestor
a := findAncestors(&home, 0)
b := findAncestors(&target, 0)
// var seen = make(map[string]int)
var found = 0
for _, ancestorA := range a {
if ancestorA.Xref == target.Xref {
relationship = ancestorA.Relationship
break
}
for _, ancestorB := range b {
if ancestorA.Xref == ancestorB.Xref {
// fmt.Printf("Found common ancestor at generation %d: %s\n", ancestorA.Generations, ancestorA.Person.Name[0].Name)
// fmt.Printf("Generation %d vs %d\n", ancestorA.Generations, ancestorB.Generations)
// fmt.Printf("Removed %d vs %d\n", ancestorA.Removed, ancestorB.Removed)
if ancestorA.Generations >= ancestorB.Generations {
removed := ancestorA.Generations - ancestorB.Generations
relationship = getChildRelationship(ancestorB.Generations, removed, &target)
} else {
removed := ancestorB.Generations - ancestorA.Generations
if ancestorA.Generations == 0 {
relationship = getSiblingChildRelationship(ancestorB.Generations, removed, &target)
} else {
relationship = getChildRelationship(ancestorA.Generations, removed, &target)
}
}
found = 1
break
}
if found == 1 {
break
}
}
}
return relationship
}
// findAncestors iterates recursively through an Individual's parents in order
// and returns a slice of their Relationships
func findAncestors(home *types.Individual, generation int) []Relationship {
var relationships []Relationship
for _, parent := range home.Parents {
if parent.Family.Husband != nil {
relation := Relationship{
Xref: parent.Family.Husband.Xref,
Person: *parent.Family.Husband,
Generations: generation,
Removed: 0,
Relationship: GetAncestorRelationship(generation+1, "M"),
}
relationships = append(relationships, relation)
relationships = append(relationships, findAncestors(parent.Family.Husband, generation+1)...)
}
if parent.Family.Wife != nil {
relation := Relationship{
Xref: parent.Family.Wife.Xref,
Person: *parent.Family.Wife,
Generations: generation,
Removed: 0,
Relationship: GetAncestorRelationship(generation+1, "F"),
}
relationships = append(relationships, relation)
relationships = append(relationships, findAncestors(parent.Family.Wife, generation+1)...)
}
}
return relationships
}
// findSiblings returns a slice of Relationship for a given Individual
func findSiblings(home *types.Individual) []Relationship {
fmt.Printf("Finding siblings for %s\n", home.Name[0].Name)
var relationships []Relationship
for _, parent := range home.Parents {
for _, child := range parent.Family.Child {
if home.Xref != child.Xref {
relation := Relationship{
Xref: child.Xref,
Person: *child,
Generations: 0,
Removed: 0,
Relationship: getSiblingRelationship(child),
}
relationships = append(relationships, relation)
}
}
}
return relationships
}
// findChildren recursively returns a slice of Relationship representing
// an Individual's children (and their children) to calculate cousinship
func findChildren(home *types.Individual, generation int, removed ...int) []Relationship {
fmt.Printf("Finding children for %s, generation %d, %d removed\n", home.Name[0].Name, generation, removed)
var relationships []Relationship
// Use a variadic param for generation so the initial call
// doesn't need to specify the generation
var _removed = 0
if len(removed) == 0 {
_removed = 0
} else {
_removed = removed[0] + 1
}
/* TODO: Figure out how to deal with step-children and half-siblings
* Maybe merge the (n) families and keep a count of how many families the
* child appears in. If occurrences < n then they are half-siblings to
*/
for _, family := range home.Family {
for _, child := range family.Family.Child {
if child != nil && home.Xref != child.Xref {
relation := Relationship{
Xref: child.Xref,
Person: *child,
Generations: generation,
Removed: _removed,
Relationship: getChildRelationship(generation, _removed, child),
}
fmt.Printf("Child: %s (%d/%d)\n", child.Name[0].Name, generation, _removed)
relationships = append(relationships, relation)
// for _, family := range child.Family {
// for _, ch := range family.Family.Child {
// relationships = append(relationships, findChildren(ch, generation, _removed)...)
// }
// }
}
}
}
return relationships
}
// findAncestralRelationship finds the relationship between an Individual and
// a common Ancestor
func findAncestralRelationship(home *types.Individual, ancestor *types.Individual, generation int) *Relationship {
var relationship = Relationship{
Xref: home.Xref,
Person: *home,
Removed: 0,
}
generation++
for _, parent := range home.Parents {
if parent.Family.Husband != nil {
if parent.Family.Husband.Xref == ancestor.Xref {
relationship.Generations = generation
relationship.Relationship = GetAncestorRelationship(generation, "M")
return &relationship
}
}
if parent.Family.Wife != nil {
if parent.Family.Wife.Xref == ancestor.Xref {
relationship.Generations = generation
relationship.Relationship = GetAncestorRelationship(generation, "F")
return &relationship
}
}
if parent.Family.Husband != nil {
var r = findAncestralRelationship(home, parent.Family.Husband, generation)
if r != nil {
return r
}
}
if parent.Family.Wife != nil {
var r = findAncestralRelationship(home, parent.Family.Wife, generation)
if r != nil {
return r
}
}
}
return nil
}
func GetAncestorRelationship(generation int, gender string) string {
var description = ""
if generation == 0 {
description = "Self"
} else if generation == 1 {
if gender == "M" {
description = "Father"
} else if gender == "F" {
description = "Mother"
} else {
description = "Parent"
}
} else if generation == 2 {
if gender == "M" {
description = "Grandfather"
} else if gender == "F" {
description = "Grandmother"
} else {
description = "Grandparent"
}
} else if generation == 3 {
if gender == "M" {
description = "Great-Grandfather"
} else if gender == "F" {
description = "Great-Grandmother"
} else {
description = "Great-Grandparent"
}
} else {
// Calculate the nth great-grandparant
if gender == "M" {
description = fmt.Sprintf("%s Great-Grandfather", humanize.Ordinal(generation-2))
} else if gender == "F" {
description = fmt.Sprintf("%s Great-Grandmother", humanize.Ordinal(generation-2))
} else {
description = fmt.Sprintf("%s Great-Grandarent", humanize.Ordinal(generation-2))
}
}
return description
}
func getSiblingRelationship(sibling *types.Individual) string {
var relation = "Sibling"
if sibling.Sex == "M" {
relation = "Brother"
} else if sibling.Sex == "F" {
relation = "Sister"
}
return relation
}
// getSiblingChildRelationship returns the relationship between self and a child of a siling
func getSiblingChildRelationship(generation int, removed int, child *types.Individual) string {
var relation = ""
if generation == 1 {
if child.Sex == "M" {
relation = "Nephew"
} else if child.Sex == "F" {
relation = "Niece"
} else {
// TODO: Find a gender-neutral term
relation = "Nephew/Niece"
}
} else if generation == 2 {
if child.Sex == "M" {
relation = "Grandnephew"
} else if child.Sex == "F" {
relation = "Grandniece"
} else {
// TODO: Find a gender-neutral term
relation = "Grandnephew/Grandniece"
}
// grand niece/nephew
} else if generation == 3 {
if child.Sex == "M" {
relation = "Great-Grandnephew"
} else if child.Sex == "F" {
relation = "Great-Grandniece"
} else {
// TODO: Find a gender-neutral term
relation = "Great-Grandnephew/Grandniece"
}
// great-grandniece/nephew
} else {
if child.Sex == "M" {
relation = fmt.Sprintf("%s Great-Grandnephew", humanize.Ordinal(removed-2))
} else if child.Sex == "F" {
relation = fmt.Sprintf("%s Great-Grandniece", humanize.Ordinal(removed-2))
} else {
// TODO: Find a gender-neutral term
relation = fmt.Sprintf("%s Great-Grandnephew/Grandniece", humanize.Ordinal(removed-2))
}
// nth great-grandniece/nephew
}
return relation
}
func getChildRelationship(generation int, removed int, child *types.Individual) string {
var relation = ""
if generation == 0 {
if removed == 0 {
if child.Sex == "M" {
relation = "Brother"
} else if child.Sex == "F" {
relation = "Sister"
}
} else if removed == 1 {
if child.Sex == "M" {
relation = "Uncle"
} else if child.Sex == "F" {
relation = "Aunt"
}
} else if removed == 2 {
if child.Sex == "M" {
relation = "Great-Uncle"
} else if child.Sex == "F" {
relation = "Great-Aunt"
}
} else if removed == 3 {
if child.Sex == "M" {
relation = "Great-Granduncle"
} else if child.Sex == "F" {
relation = "Great-Grandaunt"
}
} else {
if child.Sex == "M" {
relation = fmt.Sprintf("%s Great-Granduncle", humanize.Ordinal(removed-2))
} else if child.Sex == "F" {
relation = fmt.Sprintf("%s Great-Grandaunt", humanize.Ordinal(removed-2))
}
}
} else {
if removed == 0 {
relation = fmt.Sprintf("%s Cousin", humanize.Ordinal(generation))
} else {
relation = fmt.Sprintf("%s Cousin %dx Removed", humanize.Ordinal(generation), removed)
}
}
return relation
}
package search
import (
"fmt"
"regexp"
"strings"
"github.com/adamisrael/gedcom/date"
"github.com/adamisrael/gedcom/types"
)
func FindHomeIndividual(g types.Gedcom) *types.Individual {
// This might not work in all cases
// TODO: what if you run this against an empty gedcom? Will probably throw an exception
return g.Individual[0]
}
// FindIndividualsByNameDate finds Individuals by name, matching their year
// of birth and death to limit results
func FindIndividualsByNameDate(g types.Gedcom, name string, yob int, yod int) []types.Individual {
var individuals []types.Individual
// var err error
// var t time.Time
re := regexp.MustCompile(name)
for _, i := range g.Individual {
var birth int
var death int
if re.Find([]byte(i.Name[0].Name)) != nil {
// TODO: Implement better date handling through the Individual object
for _, event := range i.Event {
switch event.Tag {
case "BIRT":
t, err := date.Parse(event.Date)
if err == nil {
birth = t.Year()
}
case "DEAT":
t, err := date.Parse(event.Date)
if err == nil {
death = t.Year()
} else {
fmt.Printf("Failed to parse %s\n", event.Date)
}
}
}
if birth == yob && death == yod {
individuals = append(individuals, *i)
}
}
}
return individuals
}
func FindIndividualsByName(g types.Gedcom, name string) []types.Individual {
var individuals []types.Individual
// Remove extra spaces
name = strings.ReplaceAll(name, " ", " ")
name = strings.TrimSpace(name)
re := regexp.MustCompile(name)
for _, i := range g.Individual {
if re.Find([]byte(i.Name[0].Name)) != nil {
individuals = append(individuals, *i)
}
}
if len(individuals) == 0 {
fmt.Printf("Couldn't find %s\n", name)
}
return individuals
}
func FindIndividualByXref(g types.Gedcom, Xref string) *types.Individual {
for _, i := range g.Individual {
if i.Xref == Xref {
return i
}
}
return nil
}
package types
// The Address structure
type Address struct {
Full string
Line1 string
Line2 string
Line3 string
City string
State string
PostalCode string
Country string
// These can have up to three records each
Phone []string
Email []string
Fax []string
WWW []string
}
// IsValid checks if the address structure is valid
func (a Address) IsValid() bool {
valid := true
// Phone, Email, Fax, and WWW can have a max of 3
return valid
}
package types
type Association struct{}
// IsValid checks to see if the Association is valid
func (a Association) IsValid() bool {
valid := true
return valid
}
package types
type CharacterSet struct {
Name string
Version string
}
func (cs CharacterSet) IsValid() bool {
valid := false
if len(cs.Name) <= 8 {
valid = true
}
return valid
}
package types
type Citation struct {
Source *Source
Page string
Data Data
Quay string
Media []*MultiMedia
Note []*Note
}
func (c Citation) IsValid() bool {
valid := true
// if len(c.Name) <= 8 {
// valid = true
// }
return valid
}
package types
type Copyright struct {
Name string
}
func (c Copyright) IsValid() bool {
valid := true
return valid
}
package types
type Data struct {
Date string
Text []string
}
func (d Data) IsValid() bool {
valid := true
// if len(d.Data) <= 8 {
// valid = true
// }
return valid
}
package types
type Event struct {
Tag string
Value string
Type string
Date string
Place Place
Address Address
Age string
Agency string
Cause string
Citation []*Citation
Media []*MultiMedia
Note []*Note
}
func (e Event) IsValid() bool {
valid := true
return valid
}
package types
type Family struct {
Xref string
Husband *Individual
Wife *Individual
Child []*Individual
Event []*Event
}
func (f Family) IsValid() bool {
valid := true
return valid
}
package types
type FamilyLink struct {
Family *Family
Type string
Note []*Note
}
func (f FamilyLink) IsValid() bool {
valid := true
return valid
}
package types
type Gedcom struct {
Header Header
Submission *Submission
Family []*Family
Individual []*Individual
Media []*MultiMedia
Repository []*Repository
Source []*Source
Submitter []*Submitter
Trailer *Trailer
}
func (g Gedcom) IsValid() bool {
valid := true
return valid
}
package types
import "strconv"
type Header struct {
ID string
Source Source
CharacterSet CharacterSet
Version string
ProductName string
BusinessName string
BusinessAddress Address
Language string
}
func (h Header) IsValid() bool {
valid := true
if len(h.ID) > 20 {
valid = false
}
version, _ := strconv.ParseFloat(h.Version, 32)
if version < 5.5 || version >= 5.6 {
valid = false
}
return valid
}
package types
import (
"fmt"
"time"
"github.com/araddon/dateparse"
)
// Individual contains the Individual record
type Individual struct {
Xref string `json:"xref"`
Sex string `json:"sex"`
Name []*Name `json:"names"`
Event []*Event `json:"events"`
Attribute []*Event `json:"attributes"`
Parents []*FamilyLink `json:"parents"`
Family []*FamilyLink `json:"family"`
}
// IsValid performs validation against the record to
// determine if it represents a valid Individual
func (i Individual) IsValid() bool {
valid := true
return valid
}
func (i Individual) Birth() *time.Time {
var t *time.Time
for _, event := range i.Event {
switch event.Tag {
case "BIRT":
t, err := dateparse.ParseLocal(event.Date)
if err == nil {
return &t
}
}
}
return t
}
func (i Individual) Death() *time.Time {
var t *time.Time
for _, event := range i.Event {
switch event.Tag {
case "DEAT":
t, err := dateparse.ParseLocal(event.Date)
if err == nil {
return &t
}
}
}
return t
}
// Relationship calculates the relation between two individuals
func (i Individual) Relationship(b Individual) string {
return ""
}
func (i Individual) String() string {
return fmt.Sprintf("%v (%v)", i.Name[0], i.Sex)
}
// JSON returns a JSON-encoded version of the Individual record
func (i Individual) JSON() string {
return fmt.Sprintf(
`{name: "%s", sex: "%s"}`,
i.Name[0].Name,
i.Sex,
)
}
package types
type MultiMedia struct{}
func (mm MultiMedia) IsValid() bool {
valid := true
return valid
}
package types
type Name struct {
// The raw, as-is string from the GEDCOM.
Name string
// The given and surname, and suffix
Given string
Surname string
Suffix string
Citation []*Citation
Note []*Note
}
func (n Name) IsValid() bool {
valid := true
// if len(n.Name) <= 8 {
// valid = true
// }
return valid
}
// func (n Name) String() string {
// return fmt.Sprintf("%v (%v)", i.Name[0], i.Sex)
// }
package types
type Note struct {
Note string
Citation []*Citation
}
func (n Note) IsValid() bool {
valid := true
return valid
}
package types
type Place struct {
Name string
Citation []*Citation
Note []*Note
}
func (p Place) IsValid() bool {
valid := true
// if len(d.Data) <= 8 {
// valid = true
// }
return valid
}
package types
type Repository struct{}
func (r Repository) IsValid() bool {
valid := true
return valid
}
package types
type Source struct {
Xref string
Title string
Media []*MultiMedia
Note []*Note
// Name string
// Data SourceData
}
func (s Source) IsValid() bool {
valid := true
return valid
}
type SourceData struct {
Date string
Copyright string
}
func (sd SourceData) IsValid() bool {
valid := true
return valid
}
package types
type Submission struct{}
func (s Submission) IsValid() bool {
valid := true
return valid
}
package types
type Submitter struct{}
func (s Submitter) IsValid() bool {
valid := true
return valid
}
package types
type Trailer struct {
}
func (t Trailer) IsValid() bool {
valid := true
return valid
}