elliotchance/gedcom

View on GitHub
nodes.go

Summary

Maintainability
A
0 mins
Test Coverage
package gedcom

import (
    "reflect"
    "sync"
)

type Nodes []Node

// nodeCache is used by NodesWithTag. Even though the lookup of child tags are
// fairly inexpensive it happens a lot and its common for the same paths to be
// looked up many time. Especially when doing larger task like comparing GEDCOM
// files.
var nodeCache = &sync.Map{} // map[Node]map[Tag]Nodes{}

func NewNodes(ns interface{}) (nodes Nodes) {
    v := reflect.ValueOf(ns)
    for i := 0; i < v.Len(); i++ {
        nodes = append(nodes, v.Index(i).Interface().(Node))
    }

    return
}

// NodesWithTag returns the zero or more nodes that have a specific GEDCOM tag.
// If the provided node is nil then an empty slice will always be returned.
//
// If the node is nil the result will also be nil.
func NodesWithTag(node Node, tag Tag) (result Nodes) {
    if v1, ok1 := nodeCache.Load(node); ok1 {
        if v2, ok2 := v1.(*sync.Map).Load(tag); ok2 {
            return v2.(Nodes)
        }
    }

    defer func() {
        if v1, ok := nodeCache.Load(node); ok {
            v1.(*sync.Map).Store(tag, result)
        } else {
            nodeCache.Store(node, &sync.Map{})
        }
    }()

    if IsNil(node) {
        return nil
    }

    nodes := Nodes{}
    n := node.Nodes()
    for _, node := range n {
        if node.Tag().Is(tag) {
            nodes = append(nodes, node)
        }
    }

    return nodes
}

// NodesWithTagPath return 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.
//
//   birthPlaces := NodesWithTagPath(individual, TagBirth, TagPlace)
//
// If the node is nil the result will also be nil.
func NodesWithTagPath(node Node, tagPath ...Tag) Nodes {
    if IsNil(node) {
        return nil
    }

    if len(tagPath) == 0 {
        return Nodes{}
    }

    return nodesWithTagPath(node, tagPath...)
}

func nodesWithTagPath(node Node, tagPath ...Tag) Nodes {
    if len(tagPath) == 0 {
        return Nodes{node}
    }

    matches := Nodes{}

    for _, next := range NodesWithTag(node, tagPath[0]) {
        matches = append(matches, nodesWithTagPath(next, tagPath[1:]...)...)
    }

    return matches
}

// HasNestedNode checks if node contains lookingFor at any depth. If node and
// lookingFor are the same false is returned. If either node or lookingFor is
// nil then false is always returned.
//
// Nodes are matched by reference, not value so nodes that represent exactly the
// same value will not be considered equal.
func HasNestedNode(node Node, lookingFor Node) bool {
    if node == nil || lookingFor == nil {
        return false
    }

    for _, node := range node.Nodes() {
        if node == lookingFor || HasNestedNode(node, lookingFor) {
            return true
        }
    }

    return false
}

// CastTo creates a slice of a more specific node type.
//
// All Nodes must be the same type and the same as the provided t.
func (nodes Nodes) CastTo(t interface{}) interface{} {
    size := len(nodes)
    nodeType := reflect.TypeOf(t)
    sliceType := reflect.SliceOf(nodeType)
    slice := reflect.MakeSlice(sliceType, size, size)

    for i, node := range nodes {
        value := reflect.ValueOf(node)
        slice.Index(i).Set(value)
    }

    return slice.Interface()
}

func castNodesWithTag(node Node, tag Tag, t interface{}) interface{} {
    return NodesWithTag(node, tag).CastTo(t)
}

func DeleteNodesWithTag(node Node, tag Tag) {
    for _, n := range node.Nodes() {
        if n.Tag().Is(tag) {
            node.DeleteNode(n)
        }
    }
}

// FlattenAll works as Flatten with multiple inputs that are returned as a
// single slice.
//
// If any of the nodes are nil they will be ignored.
//
// The document parameter is required for copying some nodes like individuals
// that need a document context. These new nodes will be attached to the
// provided document.
func (nodes Nodes) FlattenAll(document *Document, result Nodes) {
    for _, node := range nodes {
        if IsNil(node) {
            continue
        }

        result = append(result, Flatten(document, node)...)
    }

    return
}

func (nodes Nodes) deleteNode(n Node) (Nodes, bool) {
    for i, node2 := range nodes {
        if node2 == n {
            return append(nodes[:i], nodes[i+1:]...), true
        }
    }

    return nodes, false
}