Skip to content

Commit

Permalink
feat: relate people via union
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit b9363ec
Author: Rafael Espinoza <rafael@rafaelespinoza.com>
Date:   Sat Nov 25 14:35:48 2023 -0800

    feat(cmd): Present MutualRelationship as JSON

commit 25497eb
Author: Rafael Espinoza <rafael@rafaelespinoza.com>
Date:   Tue Nov 7 14:43:33 2023 -0800

    feat(srv): Relate people via a union

commit eee10b4
Author: Rafael Espinoza <rafael@rafaelespinoza.com>
Date:   Tue Nov 7 14:34:50 2023 -0800

    feat(entity): Add affinity-related types

    These types are for relating two people via a marriage

commit ae03338
Author: Rafael Espinoza <rafael@rafaelespinoza.com>
Date:   Fri Nov 10 13:18:19 2023 -0800

    srv: Allow relating of same people

    Using this simple case to recognize spouses.

commit c66a2ee
Author: Rafael Espinoza <rafael@rafaelespinoza.com>
Date:   Sat Nov 4 21:13:19 2023 -0700

    feat: Add Spouses field to Person
  • Loading branch information
rafaelespinoza committed Nov 30, 2023
1 parent 2875fb5 commit 5abb62f
Show file tree
Hide file tree
Showing 8 changed files with 1,072 additions and 262 deletions.
84 changes: 59 additions & 25 deletions internal/cmd/relate.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Examples:
Run: func(ctx context.Context) (err error) {
var (
people []*entity.Person
r1, r2 entity.Lineage
result entity.MutualRelationship
)
switch inputFormat {
case "json":
Expand All @@ -110,33 +110,11 @@ Examples:
}
}

if r1, r2, err = srv.NewRelator(people).Relate(ctx, p1ID, p2ID); err != nil {
if result, err = srv.NewRelator(people).Relate(ctx, p1ID, p2ID); err != nil {
return
}

for _, rel := range []entity.Lineage{r1, r2} {
commonAncestors := make([]map[string]any, len(rel.CommonAncestors))

for j, person := range rel.CommonAncestors {
commonAncestors[j] = map[string]any{
"id": person.ID,
"name": person.Name,
"birth_date": formatDate(person.Birthdate),
"death_date": formatDate(person.Deathdate),
}
}

err = writeJSON(os.Stdout, map[string]any{
"description": rel.Description,
"type": rel.Type.String(),
"generations_removed": rel.GenerationsRemoved,
"common_ancestors": commonAncestors,
})
if err != nil {
return
}
}

err = writeJSON(os.Stdout, formatMutualRelationship(result))
return
},
}
Expand Down Expand Up @@ -275,3 +253,59 @@ func formatDate(in *time.Time) *string {
val := in.Format(time.DateOnly)
return &val
}

func formatMutualRelationship(in entity.MutualRelationship) (out map[string]any) {
out = map[string]any{
"union": nil,
"common_person": nil,
"relationship_1": nil,
"relationship_2": nil,
}

if in.CommonPerson != nil {
out["common_person"] = formatPerson(*in.CommonPerson)
}

if in.Union != nil {
var p1, p2 map[string]any
if in.Union.Person1 != nil {
p1 = formatPerson(*in.Union.Person1)
}
if in.Union.Person2 != nil {
p2 = formatPerson(*in.Union.Person2)
}
out["union"] = map[string]any{"person_1": p1, "person_2": p2}
}

type Tuple struct {
Dest string
Rel entity.Relationship
}

for _, tup := range []Tuple{{"relationship_1", in.R1}, {"relationship_2", in.R2}} {
path := make([]map[string]any, len(tup.Rel.Path))
for j, person := range tup.Rel.Path {
path[j] = formatPerson(person)
}

out[tup.Dest] = map[string]any{
"description": tup.Rel.Description,
"type": tup.Rel.Type.String(),
"generations_removed": tup.Rel.GenerationsRemoved,
"path": path,
"source_id": tup.Rel.SourceID,
"target_id": tup.Rel.TargetID,
}
}

return
}

func formatPerson(p entity.Person) (out map[string]any) {
return map[string]any{
"id": p.ID,
"name": p.Name,
"birth_date": formatDate(p.Birthdate),
"death_date": formatDate(p.Deathdate),
}
}
38 changes: 0 additions & 38 deletions internal/entity/lineage.go

This file was deleted.

1 change: 1 addition & 0 deletions internal/entity/person.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ type Person struct {
Deathdate *time.Time
Parents []*Person
Children []*Person
Spouses []*Person
}
91 changes: 91 additions & 0 deletions internal/entity/relationship.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package entity

// MutualRelationship describes a relationship from person A to person B and its
// complementary relationship from person B to person A.
//
// If person A and person B have a blood relationship, then CommonPerson is a
// shared common ancestor. The Path fields in each Relationship denote the
// ancestral path to the CommonPerson, where the first person in the Path is
// starting person and the last person is the CommonPerson.
//
// If person A and person B are related by marriage, then Union is non-empty.
// The Path fields in each Relationship describe how the starting person relates
// to a person in the Union.
type MutualRelationship struct {
CommonPerson *Person
Union *Union
R1, R2 Relationship
}

// Relationship is a unidirectional descriptor how a person at SourceID relates
// to a person at TargetID. The connection may have a consanguineous (by blood)
// nature, or may be affinal (by law, or via marriage).
type Relationship struct {
// SourceID identifies the person representing the vantage point in this Relationship.
SourceID string
// TargetID identifies the person who is being related to by the person at SourceID.
TargetID string
// Type may indicate if the relationship is consanguineous (by blood),
// affinal (by law, or via marriage), or is unknown.
Type RelationshipType
// Description elaborates on the Type.
Description string
GenerationsRemoved int
// Path is the path to a common person if the Type field indicates a
// consanguineous relationship. If the Type indiciates an affinal
// relationship, then Path is the path from the person at SourceID to a
// person in a Union.
Path []Person
}

type RelationshipType int

const (
Unknown RelationshipType = iota

// These values indicate a consanguineous (by blood) relationship.
Self
Sibling
Child
Parent
AuntUncle
Cousin
NieceNephew

// These values indicate an affinal (by marriage) relationship.
Spouse
SiblingInLaw
ChildInLaw
ParentInLaw
AuntUncleInLaw
CousinInLaw
NieceNephewInLaw
)

var relationshipNames = []string{
"unknown",

"self",
"sibling",
"child",
"parent",
"aunt/uncle",
"cousin",
"niece/nephew",

"spouse",
"sibling in-law",
"child in-law",
"parent in-law",
"aunt/uncle in-law",
"cousin in-law",
"niece/nephew in-law",
}

func (t RelationshipType) String() string {
if t < 0 || int(t) >= len(relationshipNames) {
return ""
}

return relationshipNames[t]
}
21 changes: 17 additions & 4 deletions internal/srv/parse_gedcom.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,24 @@ func convertGedcomPeople(ctx context.Context, records []*gedcom.IndividualRecord
}

childTuples := make([]*entity.Person, 0, len(individual.FamiliesAsPartner)*2)
spouseTuples := make([]*entity.Person, 0, len(individual.FamiliesAsPartner))
for _, famID := range individual.FamiliesAsPartner {
familyRecord, ok := gedcomFamiliesByID[famID]
if !ok {
return nil, fmt.Errorf("gedcom family as partner %q not found for individual %q", famID, individual.Xref)
}

for _, spouseID := range familyRecord.ParentXrefs {
spouse, ok := out[spouseID]
if !ok {
return nil, fmt.Errorf("entity spouse %q from family %q not found for individual as partner %q", spouseID, famID, individual.Xref)
}
if spouseID == individual.Xref {
continue
}
spouseTuples = append(spouseTuples, simplifyPerson(spouse))
}

for _, childID := range familyRecord.ChildXrefs {
child, ok := out[childID]
if !ok {
Expand All @@ -121,6 +133,7 @@ func convertGedcomPeople(ctx context.Context, records []*gedcom.IndividualRecord
person := out[individual.Xref]
person.Parents = slices.Clip(parentTuples)
person.Children = slices.Clip(childTuples)
person.Spouses = slices.Clip(spouseTuples)
out[individual.Xref] = person
}

Expand Down Expand Up @@ -190,10 +203,10 @@ func convertGedcomFamilies(ctx context.Context, records []*gedcom.FamilyRecord,
return out, nil
}

// simplifyPerson intentionally does not copy the Children or Parent fields to
// help keep each output item succinct. This is most beneficial when marshaling
// the results. Without such a limit, you could end up with generations upon
// generations of deeply-nested structures.
// simplifyPerson intentionally does not copy the Children, Parent, or Spouses
// fields to help keep each output item succinct. This is most beneficial when
// marshaling the results. Without such a limit, you could end up with
// generations upon generations of deeply-nested structures.
func simplifyPerson(in *entity.Person) *entity.Person {
return &entity.Person{
ID: in.ID,
Expand Down

0 comments on commit 5abb62f

Please sign in to comment.