Repository: elliotchance/gedcom (go)

Issue:
Please add support for unofficial _MARNM tag
Geni has two fields to describe individual's last name: "birth surname" and "last name". In order to express it in GEDCOM, they are using an unofficial _MARNM tag which is populated with "last name" from their record, while SURN is always populated with "birth surname" (if available). You can actually choose which one of those goes into the NAME field between the slashes, but there is currently no way to extract the value of _MARNM tag using the query language. I would appreciate having such functionality, as it would make much easier for me to run various analysis scripts.

Pull request description:
Adding NodesWithTagPath() to q
NodesWithTagPath returns all of the nodes that have an exact tag path.
The number of nodes returned can be zero and tag must match the tag path
completely and exactly.

Find all Death nodes that belong to all individuals:

    .Individuals | NodesWithTagPath("DEAT")

From the individuals find all the Date nodes within only the Birth nodes.

    .Individuals | NodesWithTagPath("BIRT", "DATE")

Combine all of the birth and death dates:

    Births are .Individuals | NodesWithTagPath("BIRT", "DATE") | {type: "birth", date: .String};
    Deaths are .Individuals | NodesWithTagPath("DEAT", "DATE") | {type: "death", date: .String};
    Combine(Births, Deaths)

Fixes #300

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/elliotchance/gedcom/303)
<!-- Reviewable:end -->


Patch:
diff --git a/q/doc.go b/q/doc.go
index 0e5a16f..420aef2 100644
--- a/q/doc.go
+++ b/q/doc.go
@@ -113,6 +113,28 @@
 //
 // Merges two documents while also merging similar individuals.
 //
+//   NodesWithTagPaths(Tags...)
+//
+// NodesWithTagPath returns all of the nodes that have an exact tag path. The
+// number of nodes returned can be zero and tag must match the tag path
+// completely and exactly.
+//
+// Find all Death nodes that belong to all individuals:
+//
+//   .Individuals | NodesWithTagPath("DEAT")
+//
+// From the individuals find all the Date nodes within only the Birth nodes.
+//
+//   .Individuals | NodesWithTagPath("BIRT", "DATE")
+//
+// Combine all of the birth and death dates:
+//
+//   Births are .Individuals | NodesWithTagPath("BIRT", "DATE") | {type: "birth", date: .String};
+//   Deaths are .Individuals | NodesWithTagPath("DEAT", "DATE") | {type: "death", date: .String};
+//   Combine(Births, Deaths)
+//
+// If the node is nil the result will also be nil.
+//
 //   Only(condition)
 //
 // The Only function returns a new slice that only contains the entities that
diff --git a/q/functions.go b/q/functions.go
index 57b2b92..fef3c9a 100644
--- a/q/functions.go
+++ b/q/functions.go
@@ -10,5 +10,6 @@ var Functions = map[string]Expression{
 	"Last":                         &LastExpr{},
 	"Length":                       &LengthExpr{},
 	"MergeDocumentsAndIndividuals": &MergeDocumentsAndIndividualsExpr{},
+	"NodesWithTagPath":             &NodesWithTagPathExpr{},
 	"Only":                         &OnlyExpr{},
 }
diff --git a/q/nodes_with_tag_path.go b/q/nodes_with_tag_path.go
new file mode 100644
index 0000000..d67bbb7
--- /dev/null
+++ b/q/nodes_with_tag_path.go
@@ -0,0 +1,70 @@
+package q
+
+import (
+	"fmt"
+	"github.com/elliotchance/gedcom"
+	"reflect"
+)
+
+// NodesWithTagPathExpr is a function. See Evaluate.
+type NodesWithTagPathExpr struct{}
+
+// NodesWithTagPath returns all of the nodes that have an exact tag path. The
+// number of nodes returned can be zero and tag must match the tag path
+// completely and exactly.
+//
+// If the node is nil the result will also be nil.
+//
+// Find all Death nodes that belong to all individuals:
+//
+//   .Individuals | NodesWithTagPath("DEAT")
+//
+// From the individuals find all the Date nodes within only the Birth nodes.
+//
+//   .Individuals | NodesWithTagPath("BIRT", "DATE")
+//
+// Combine all of the birth and death dates:
+//
+//   Births are .Individuals | NodesWithTagPath("BIRT", "DATE") | {type: "birth", date: .String};
+//   Deaths are .Individuals | NodesWithTagPath("DEAT", "DATE") | {type: "death", date: .String};
+//   Combine(Births, Deaths)
+//
+func (e *NodesWithTagPathExpr) Evaluate(engine *Engine, input interface{}, args []*Statement) (interface{}, error) {
+	in := reflect.ValueOf(input)
+
+	if input == nil || in.IsNil() {
+		return gedcom.Nodes(nil), nil
+	}
+
+	// Convert into a slice if needed.
+	if in.Kind() != reflect.Slice {
+		s := reflect.MakeSlice(reflect.SliceOf(in.Type()), 1, 1)
+		s.Index(0).Set(in)
+		in = reflect.ValueOf(s.Interface())
+	}
+
+	// Process args.
+	var argValues []gedcom.Tag
+	for _, arg := range args {
+		argValue, err := arg.Evaluate(engine, nil)
+		if err != nil {
+			return gedcom.Nodes(nil), err
+		}
+
+		stringForTag, ok := argValue.(string)
+		if !ok {
+			stringForTag = fmt.Sprintf("%s", argValue)
+		}
+
+		argValues = append(argValues, gedcom.TagFromString(stringForTag))
+	}
+
+	// Process slice input.
+	var results gedcom.Nodes
+	for i := 0; i < in.Len(); i++ {
+		node := in.Index(i).Interface().(gedcom.Node)
+		results = append(results, gedcom.NodesWithTagPath(node, argValues...)...)
+	}
+
+	return results, nil
+}


Test patch:
diff --git a/q/nodes_with_tag_path_test.go b/q/nodes_with_tag_path_test.go
new file mode 100644
index 0000000..6f99c0e
--- /dev/null
+++ b/q/nodes_with_tag_path_test.go
@@ -0,0 +1,103 @@
+package q_test
+
+import (
+	"github.com/elliotchance/gedcom"
+	"github.com/elliotchance/gedcom/q"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestNodesWithTagPathExpr_Evaluate(t *testing.T) {
+	engine := &q.Engine{}
+
+	doc := gedcom.NewDocument()
+	individual := doc.AddIndividual("P1")
+	individual.AddBirthDate("16 Apr 1973")
+	individual2 := doc.AddIndividual("P2")
+	individual2.AddBirthDate("8 Mar 1884")
+
+	for testName, test := range map[string]struct {
+		input          interface{}
+		args           []*q.Statement
+		expectedGEDCOM string
+	}{
+		"Nil": {
+			input:          nil,
+			args:           nil,
+			expectedGEDCOM: "",
+		},
+		"IndividualNone": {
+			input:          individual,
+			args:           nil,
+			expectedGEDCOM: "",
+		},
+		"IndividualBIRT": {
+			input: individual,
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "BIRT"}}},
+			},
+			expectedGEDCOM: "0 BIRT\n1 DATE 16 Apr 1973\n",
+		},
+		"IndividualDATE": {
+			input: individual,
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "DATE"}}},
+			},
+			expectedGEDCOM: "",
+		},
+		"IndividualBIRTDATE": {
+			input: individual,
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "BIRT"}}},
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "DATE"}}},
+			},
+			expectedGEDCOM: "0 DATE 16 Apr 1973\n",
+		},
+		"IndividualBIRTDEAT": {
+			input: individual,
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "BIRT"}}},
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "DEAT"}}},
+			},
+			expectedGEDCOM: "",
+		},
+		"IndividualsBIRT": {
+			input: doc.Individuals(),
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "BIRT"}}},
+			},
+			expectedGEDCOM: "0 BIRT\n1 DATE 16 Apr 1973\n0 BIRT\n1 DATE 8 Mar 1884\n",
+		},
+		"IndividualsDATE": {
+			input: doc.Individuals(),
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "DATE"}}},
+			},
+			expectedGEDCOM: "",
+		},
+		"IndividualsBIRTDATE": {
+			input: doc.Individuals(),
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "BIRT"}}},
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "DATE"}}},
+			},
+			expectedGEDCOM: "0 DATE 16 Apr 1973\n0 DATE 8 Mar 1884\n",
+		},
+		"IndividualsBIRTDEAT": {
+			input: doc.Individuals(),
+			args: []*q.Statement{
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "BIRT"}}},
+				{Expressions: []q.Expression{&q.ConstantExpr{Value: "DEAT"}}},
+			},
+			expectedGEDCOM: "",
+		},
+	} {
+		t.Run(testName, func(t *testing.T) {
+			actual, err := (&q.NodesWithTagPathExpr{}).Evaluate(engine, test.input, test.args)
+			require.NoError(t, err)
+
+			assert.Equal(t, test.expectedGEDCOM, gedcom.NewDocumentWithNodes(actual.(gedcom.Nodes)).String())
+		})
+	}
+}
diff --git a/q/question_mark_expr_test.go b/q/question_mark_expr_test.go
index bd0116c..bfb51b6 100644
--- a/q/question_mark_expr_test.go
+++ b/q/question_mark_expr_test.go
@@ -31,6 +31,7 @@ var functionAndVariableChoices = []string{
 	"Last",
 	"Length",
 	"MergeDocumentsAndIndividuals",
+	"NodesWithTagPath",
 	"Only",
 }