All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.collections.PathTrie.kt Maven / Gradle / Ivy

There is a newer version: 0.23.0
Show newest version
package io.kform.collections

import io.kform.AbsolutePath
import io.kform.AbsolutePathFragment
import io.kform.Path
import io.kform.toAbsolutePath

/** Entry in a path trie holding values of type [T]. */
public data class PathTrieEntry(
    override val path: AbsolutePath,
    override val value: T,
    override val id: PathMultimapEntryId
) : PathMultimapEntry

/**
 * Implementation of a mutable path multimap as a trie. Held values are of type [T].
 *
 * Each node of the trie represents a path fragment. As such, a path can be represented by
 * navigating through the trie's nodes. Each node contains the values associated with the path they
 * represent.
 *
 * A trie is a good structure to implement a path multimap due to the intended semantics of the
 * [get(path)][get] and [remove(path)][removeEntry] functions: the `get` function should return all
 * values matching a given path, whilst the `remove` function removes values contained by a given
 * path (following the [matching][AbsolutePath.matches] and [containment][AbsolutePath.contains]
 * semantics of paths).
 *
 * As an example, imagine the following operations on the trie (with simplified notation for the
 * paths):
 * ```kotlin
 * trie["/"] = 0
 * trie["/path/x"] = 1
 * trie["/path/y"] = 2
 * trie["/path/x/z"] = 3
 * trie["/∗/path"] = 4
 * trie["/∗/path"] = 5
 * ```
 *
 * The resulting structure would look similar to the following, where the right side represents the
 * values associated with each trie node:
 * ```
 * /          -> [0]
 * ├─ path    -> []
 * │  ├─ x    -> [1]
 * │  │  └─ z -> [3]
 * │  └─ y    -> [2]
 * └─ ∗       -> []
 *    └─ path -> [4, 5]
 * ```
 *
 * Getting values associated with the path `"/∗/∗"` would return a sequence with values `[1, 2, 4,
 * 5]`.
 *
 * @constructor Creates a new path trie given an `initialCapacity` (defaults to 100).
 */
public class PathTrie(initialCapacity: Int = 100) : MutablePathMultimap {
    /**
     * Class representing a node of the trie representing the fragment [fragment] and whose parent
     * node is [parent].
     */
    private class PathTrieNode(
        val parent: PathTrieNode? = null,
        val fragment: AbsolutePathFragment? = null
    ) {
        /** Child nodes. */
        val childNodes: MutableMap> =
            HashMap(CHILD_NODES_MAP_INITIAL_CAPACITY)

        /** Entries of this node. */
        val entries: MutableMap> =
            HashMap(ENTRIES_MAP_INITIAL_CAPACITY)

        /** Creates and returns a new child node for the fragment [fragment]. */
        fun newChildNode(fragment: AbsolutePathFragment): PathTrieNode {
            val newNode = PathTrieNode(this, fragment)
            childNodes[fragment] = newNode
            return newNode
        }

        /** Clears this node. */
        fun clear() {
            childNodes.clear()
            entries.clear()
        }

        /** Cleans up this node by removing itself from its parent when it is empty. */
        fun cleanUp() {
            if (parent != null && childNodes.isEmpty() && entries.isEmpty()) {
                parent.childNodes.remove(fragment!!)
                parent.cleanUp()
            }
        }

        private companion object {
            // Initial capacities for the node maps
            const val CHILD_NODES_MAP_INITIAL_CAPACITY = 10

            // Assumes that typically only one value is associated with each
            const val ENTRIES_MAP_INITIAL_CAPACITY = 1
        }
    }

    /** Root node of the trie. */
    private val rootNode: PathTrieNode = PathTrieNode()

    /** Nodes containing each key, for fast access via key. */
    private val nodesById: MutableMap> =
        HashMap(initialCapacity)

    /** Next entry identifier to use. */
    private var entryId: PathMultimapEntryId = 0

    override val size: Int
        get() = nodesById.size

    /**
     * Formats the trie as a mapping of paths to the list of values associated with them.
     *
     * Example: `"{/path1=[val1, val2], /path2=[val3, val4]}"`.
     */
    override fun toString(): String = toMap().toString()

    override fun containsPath(path: Path): Boolean =
        path.toAbsolutePath().let { absolutePath -> entriesImpl(absolutePath).any() }

    override fun containsEntry(entryId: PathMultimapEntryId): Boolean =
        nodesById.containsKey(entryId)

    override fun containsValue(value: T): Boolean =
        entriesImpl(AbsolutePath.MATCH_ALL).any { (_, value2) -> value == value2 }

    override fun get(path: Path): Sequence =
        path.toAbsolutePath().let { absolutePath ->
            entriesImpl(absolutePath).map { (_, value) -> value }
        }

    override fun getEntry(entryId: PathMultimapEntryId): PathTrieEntry? {
        val node = nodesById[entryId] ?: return null
        val pair = node.entries[entryId]!!
        return PathTrieEntry(pair.first, pair.second, entryId)
    }

    override fun put(path: Path, value: T): PathMultimapEntryId =
        path.toAbsolutePath().let { absolutePath ->
            val newId = entryId++
            var curNode = this.rootNode
            for (fragment in absolutePath) {
                var nextNode = curNode.childNodes[fragment]
                if (nextNode == null) {
                    nextNode = curNode.newChildNode(fragment)
                }
                curNode = nextNode
            }
            nodesById[newId] = curNode
            curNode.entries[newId] = absolutePath to value
            return newId
        }

    override fun remove(path: Path): List> =
        path.toAbsolutePath().let { absolutePath ->
            val removedEntries = mutableListOf>()
            val cache = mutableSetOf>()
            removeImpl(absolutePath, removedEntries, cache)
            // Clean up all nodes from which we may have removed an entry since we cannot clean them
            // up
            // whilst iterating on them in [removeImpl]
            for (node in cache) {
                node.cleanUp()
            }
            return removedEntries
        }

    override fun removeEntry(entryId: PathMultimapEntryId): PathTrieEntry? {
        val node = nodesById[entryId] ?: return null
        val pair = node.entries.remove(entryId)!!
        node.cleanUp()
        nodesById.remove(entryId)
        return PathTrieEntry(pair.first, pair.second, entryId)
    }

    override fun clear() {
        rootNode.clear()
        nodesById.clear()
    }

    override fun entries(path: Path): Sequence> =
        path.toAbsolutePath().let { absolutePath -> entriesImpl(absolutePath) }

    /**
     * Returns a sequence over all entries matching [path] (recursive implementation).
     *
     * @param cache Set containing the nodes whose entries we've already yielded. The algorithm can
     *   end up analysing the same node more than once, hence the need to keep track of the nodes
     *   with entries already yielded.
     * @param i Index of the fragment of the path being analysed.
     * @param node Current node being analysed.
     */
    private fun entriesImpl(
        path: AbsolutePath,
        cache: MutableSet> = mutableSetOf(),
        i: Int = 0,
        node: PathTrieNode = rootNode
    ): Sequence> = sequence {
        // Base case (whole path matched)
        if (i == path.size) {
            if (!cache.contains(node)) {
                cache += node
                for ((id, pair) in node.entries) {
                    yield(PathTrieEntry(pair.first, pair.second, id))
                }
            }
        } else {
            when (val fragment = path[i]) {
                AbsolutePathFragment.RecursiveWildcard -> {
                    // Match next fragment against same node
                    yieldAll(entriesImpl(path, cache, i + 1, node))
                    // Match same fragment against all next nodes except the recursive wildcard one
                    // (which is matched against at the end of the method)
                    for ((nextFragment, nextNode) in node.childNodes.entries) {
                        if (nextFragment != AbsolutePathFragment.RecursiveWildcard) {
                            yieldAll(entriesImpl(path, cache, i, nextNode))
                        }
                    }
                }
                AbsolutePathFragment.Wildcard -> {
                    // Match next fragment against all next nodes except the recursive wildcard one
                    // (which is matched against at the end of the method)
                    for ((nextFragment, nextNode) in node.childNodes.entries) {
                        if (nextFragment !== AbsolutePathFragment.RecursiveWildcard) {
                            yieldAll(entriesImpl(path, cache, i + 1, nextNode))
                        }
                    }
                }
                else -> {
                    // Match next fragment against matching node
                    val nextNode = node.childNodes[fragment]
                    if (nextNode != null) {
                        yieldAll(entriesImpl(path, cache, i + 1, nextNode))
                    }
                    // Match next fragment against non-recursive wildcard node
                    val wildcardNode = node.childNodes[AbsolutePathFragment.Wildcard]
                    if (wildcardNode != null) {
                        yieldAll(entriesImpl(path, cache, i + 1, wildcardNode))
                    }
                }
            }
        }

        // Match same fragment and all next fragments against the recursive wildcard node
        val recWildcardNode = node.childNodes[AbsolutePathFragment.RecursiveWildcard]
        if (recWildcardNode != null) {
            for (j in i..path.size) {
                yieldAll(entriesImpl(path, cache, j, recWildcardNode))
            }
        }
    }

    /**
     * Removes all entries with a path contained by [path] and adds them to a list of removed
     * entries provided in [removedEntries].
     *
     * Because we cannot clean up nodes whilst iterating on them, this function's caller should
     * clean up all nodes in the [cache] after calling this function (and as such it has to provide
     * the cache itself as well).
     *
     * @param cache Set containing the nodes whose entries we've already removed. The algorithm can
     *   end up analysing the same node more than once, hence the need to keep track of the nodes
     *   with entries already removed.
     * @param i Index of the fragment of the path being analysed.
     * @param node Current node being analysed.
     */
    private fun removeImpl(
        path: AbsolutePath,
        removedEntries: MutableList>,
        cache: MutableSet>,
        i: Int = 0,
        node: PathTrieNode = rootNode
    ) {
        // Base case (whole path matched)
        if (i == path.size) {
            if (!cache.contains(node)) {
                cache += node
                if (node.entries.isNotEmpty()) {
                    for ((id, pair) in node.entries) {
                        removedEntries += PathTrieEntry(pair.first, pair.second, id)
                        nodesById.remove(id)
                    }
                    node.entries.clear()
                }
            }
        } else {
            when (val fragment = path[i]) {
                AbsolutePathFragment.RecursiveWildcard -> {
                    // Match next fragment against same node
                    removeImpl(path, removedEntries, cache, i + 1, node)
                    // Match same fragment against all next nodes
                    for (nextNode in node.childNodes.values) {
                        removeImpl(path, removedEntries, cache, i, nextNode)
                    }
                }
                AbsolutePathFragment.Wildcard -> {
                    // Match next fragment against all next nodes except the recursive wildcard one
                    for ((nextFragment, nextNode) in node.childNodes.entries) {
                        if (nextFragment != AbsolutePathFragment.RecursiveWildcard) {
                            removeImpl(path, removedEntries, cache, i + 1, nextNode)
                        }
                    }
                }
                else -> {
                    // Match next fragment against matching node
                    val nextNode = node.childNodes[fragment]
                    if (nextNode != null) {
                        removeImpl(path, removedEntries, cache, i + 1, nextNode)
                    }
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy