Version 1
[yaffs-website] / vendor / easyrdf / easyrdf / lib / EasyRdf / Isomorphic.php
diff --git a/vendor/easyrdf/easyrdf/lib/EasyRdf/Isomorphic.php b/vendor/easyrdf/easyrdf/lib/EasyRdf/Isomorphic.php
new file mode 100644 (file)
index 0000000..5b46125
--- /dev/null
@@ -0,0 +1,436 @@
+<?php
+
+/**
+ * EasyRdf
+ *
+ * LICENSE
+ *
+ * Copyright (c) 2013 Nicholas J Humfrey.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
+ *    promote products derived from this software without specific prior
+ *    written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @package    EasyRdf
+ * @copyright  Copyright (c) 2013 Nicholas J Humfrey
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ */
+
+/**
+ * Functions for comparing two graphs with each other
+ *
+ * Based on rdf-isomorphic.rb by Ben Lavender:
+ * https://github.com/ruby-rdf/rdf-isomorphic
+ *
+ * @package    EasyRdf
+ * @copyright  Copyright (c) 2013 Nicholas J Humfrey
+ * @license    http://www.opensource.org/licenses/bsd-license.php
+ */
+class EasyRdf_Isomorphic
+{
+    /**
+     * Check if one graph is isomorphic (equal) to another graph
+     *
+     * For example:
+     *    $graphA = EasyRdf_Graph::newAndLoad('http://example.com/a.ttl');
+     *    $graphB = EasyRdf_Graph::newAndLoad('http://example.com/b.ttl');
+     *    if (EasyRdf_Isomorphic::isomorphic($graphA, $graphB)) print "Equal!";
+     *
+     * @param  object EasyRdf_Graph  $graphA  The first graph to be compared
+     * @param  object EasyRdf_Graph  $graphB  The second graph to be compared
+     * @return boolean True if the two graphs are isomorphic
+     */
+    public static function isomorphic($graphA, $graphB)
+    {
+        return is_array(self::bijectionBetween($graphA, $graphB));
+    }
+
+    /**
+     * Returns an associative array of bnode identifiers representing an isomorphic
+     * bijection of one EasyRdf_Graph to another EasyRdf_Graph's blank nodes or
+     * null if a bijection cannot be found.
+     *
+     * @param  object EasyRdf_Graph  $graphA  The first graph to be compared
+     * @param  object EasyRdf_Graph  $graphB  The second graph to be compared
+     * @return array bnode mapping from $graphA to $graphB
+     */
+    public static function bijectionBetween($graphA, $graphB)
+    {
+        $bnodesA = array();
+        $bnodesB = array();
+        $statementsA = array();
+        $statementsB = array();
+
+        // Quick initial check: are there differing numbers of subjects?
+        if (self::countSubjects($graphA) != self::countSubjects($graphB)) {
+            return null;
+        }
+
+        // Check if all the statements in Graph A exist in Graph B
+        $groundedStatementsMatch = self::groundedStatementsMatch($graphA, $graphB, $bnodesA, $statementsA);
+
+        if ($groundedStatementsMatch) {
+            // Check if all the statements in Graph B exist in Graph A
+            $groundedStatementsMatch = self::groundedStatementsMatch($graphB, $graphA, $bnodesB, $statementsB);
+        }
+
+        if ($groundedStatementsMatch === false) {
+            // The grounded statements do not match
+            return null;
+        } elseif (count($bnodesA) > 0 or count($bnodesB > 0)) {
+            // There are blank nodes - build a bi-jection
+            return self::buildBijectionTo($statementsA, $bnodesA, $statementsB, $bnodesB);
+        } else {
+            // No bnodes and the grounded statements match
+            return array();
+        }
+    }
+
+    /**
+     * Count the number of subjects in a graph
+     * @ignore
+     */
+    private static function countSubjects($graph)
+    {
+        return count($graph->toRdfPhp());
+    }
+
+    /**
+     * Check if all the statements in $graphA also appear in $graphB
+     * @ignore
+     */
+    private static function groundedStatementsMatch($graphA, $graphB, &$bnodes, &$anonStatements)
+    {
+        $groundedStatementsMatch = true;
+
+        foreach ($graphA->toRdfPhp() as $subject => $properties) {
+            if (substr($subject, 0, 2) == '_:') {
+                array_push($bnodes, $subject);
+                $subjectIsBnode = true;
+            } else {
+                $subjectIsBnode = false;
+            }
+
+            foreach ($properties as $property => $values) {
+                foreach ($values as $value) {
+                    if ($value['type'] == 'uri' and substr($value['value'], 0, 2) == '_:') {
+                        array_push($bnodes, $value['value']);
+                        $objectIsBnode = true;
+                    } else {
+                        $objectIsBnode = false;
+                    }
+
+                    if ($groundedStatementsMatch and
+                        $subjectIsBnode === false and
+                        $objectIsBnode === false and
+                        $graphB->hasProperty($subject, $property, $value) === false
+                    ) {
+                        $groundedStatementsMatch = false;
+                    }
+
+                    if ($subjectIsBnode or $objectIsBnode) {
+                        array_push(
+                            $anonStatements,
+                            array(
+                                array('type' => $subjectIsBnode ? 'bnode' : 'uri', 'value' => $subject),
+                                array('type' => 'uri', 'value' => $property),
+                                $value
+                            )
+                        );
+                    }
+                }
+            }
+        }
+
+        return $groundedStatementsMatch;
+    }
+
+    /**
+     * The main recursive bijection algorithm.
+     *
+     * This algorithm is very similar to the one explained by Jeremy Carroll in
+     * http://www.hpl.hp.com/techreports/2001/HPL-2001-293.pdf. Page 12 has the
+     * relevant pseudocode.
+     *
+     * @ignore
+     */
+    private static function buildBijectionTo
+    (
+        $statementsA,
+        $nodesA,
+        $statementsB,
+        $nodesB,
+        $groundedHashesA = array(),
+        $groundedHashesB = array()
+    ) {
+
+        // Create a hash signature of every node, based on the signature of
+        // statements it exists in.
+        // We also save hashes of nodes that cannot be reliably known; we will use
+        // that information to eliminate possible recursion combinations.
+        //
+        // Any mappings given in the method parameters are considered grounded.
+        list($hashesA, $ungroundedHashesA) = self::hashNodes($statementsA, $nodesA, $groundedHashesA);
+        list($hashesB, $ungroundedHashesB) = self::hashNodes($statementsB, $nodesB, $groundedHashesB);
+
+        // Grounded hashes are built at the same rate between the two graphs (if
+        // they are isomorphic).  If there exists a grounded node in one that is
+        // not in the other, we can just return.  Ungrounded nodes might still
+        // conflict, so we don't check them.  This is a little bit messy in the
+        // middle of the method, and probably slows down isomorphic checks,  but
+        // prevents almost-isomorphic cases from getting nutty.
+        foreach ($hashesA as $nodeA => $hashA) {
+            if (!in_array($hashA, $hashesB)) {
+                return null;
+            }
+        }
+        foreach ($hashesB as $nodeB => $hashB) {
+            if (!in_array($hashB, $hashesA)) {
+                return null;
+            }
+        }
+
+        // Using the created hashes, map nodes to other_nodes
+        // Ungrounded hashes will also be equal, but we keep the distinction
+        // around for when we recurse later (we only recurse on ungrounded nodes)
+        $bijection = array();
+        foreach ($nodesA as $nodeA) {
+            $foundNode = null;
+            foreach ($ungroundedHashesB as $nodeB => $hashB) {
+                if ($ungroundedHashesA[$nodeA] == $hashB) {
+                    $foundNode = $nodeB;
+                }
+            }
+
+            if ($foundNode) {
+                $bijection[$nodeA] = $foundNode;
+
+                // Deletion is required to keep counts even; two nodes with identical
+                // signatures can biject to each other at random.
+                unset($ungroundedHashesB[$foundNode]);
+            }
+        }
+
+        // bijection is now a mapping of nodes to other_nodes.  If all are
+        // accounted for on both sides, we have a bijection.
+        //
+        // If not, we will speculatively mark pairs with matching ungrounded
+        // hashes as bijected and recurse.
+        $bijectionA = array_keys($bijection);
+        $bijectionB = array_values($bijection);
+        sort($bijectionA);
+        sort($nodesA);
+        sort($bijectionB);
+        sort($nodesB);
+        if ($bijectionA != $nodesA or $bijectionB != $nodesB) {
+            $bijection = null;
+
+            foreach ($nodesA as $nodeA) {
+
+                // We don't replace grounded nodes' hashes
+                if (isset($hashesA[$nodeA])) {
+                    continue;
+                }
+
+                foreach ($nodesB as $nodeB) {
+                    // We don't replace grounded nodesB's hashes
+                    if (isset($hashesB[$nodeB])) {
+                        continue;
+                    }
+
+                    // The ungrounded signature must match for this to potentially work
+                    if ($ungroundedHashesA[$nodeA] != $ungroundedHashesB[$nodeB]) {
+                        continue;
+                    }
+
+                    $hash = sha1($nodeA);
+                    $hashesA[$nodeA] = $hash;
+                    $hashesB[$nodeB] = $hash;
+                    $bijection = self::buildBijectionTo(
+                        $statementsA,
+                        $nodesA,
+                        $statementsB,
+                        $nodesA,
+                        $hashesA,
+                        $hashesB
+                    );
+                }
+            }
+        }
+
+        return $bijection;
+    }
+
+    /**
+     * Given a set of statements, create a mapping of node => SHA1 for a given
+     * set of blank nodes.  grounded_hashes is a mapping of node => SHA1 pairs
+     * that we will take as a given, and use those to make more specific
+     * signatures of other nodes.
+     *
+     * Returns a tuple of associative arrats: one of grounded hashes, and one of all
+     * hashes.  grounded hashes are based on non-blank nodes and grounded blank
+     * nodes, and can be used to determine if a node's signature matches
+     * another.
+     *
+     * @ignore
+     */
+    private static function hashNodes($statements, $nodes, $groundedHahes)
+    {
+        $hashes = $groundedHahes;
+        $ungroundedHashes = array();
+        $hashNeeded = true;
+
+        // We may have to go over the list multiple times.  If a node is marked as
+        // grounded, other nodes can then use it to decide their own state of
+        // grounded.
+        while ($hashNeeded) {
+            $startingGroundedNodes = count($hashes);
+            foreach ($nodes as $node) {
+                if (!isset($hashes[$node])) {
+                    $hash = self::nodeHashFor($node, $statements, $hashes);
+                    if (self::nodeIsGrounded($node, $statements, $hashes)) {
+                        $hashes[$node] = $hash;
+                    }
+                }
+                $ungroundedHashes[$node] = $hash;
+            }
+
+            // after going over the list, any nodes with a unique hash can be marked
+            // as grounded, even if we have not tied them back to a root yet.
+            $uniques = array();
+            foreach ($ungroundedHashes as $node => $hash) {
+                $uniques[$hash] = isset($uniques[$hash]) ? false : $node;
+            }
+            foreach ($uniques as $hash => $node) {
+                if ($node) {
+                    $hashes[$node] = $hash;
+                }
+            }
+            $hashNeeded = ($startingGroundedNodes != count($hashes));
+        }
+
+        return array($hashes, $ungroundedHashes);
+    }
+
+    /**
+     * Generate a hash for a node based on the signature of the statements it
+     * appears in.  Signatures consist of grounded elements in statements
+     * associated with a node, that is, anything but an ungrounded anonymous
+     * node.  Creating the hash is simply hashing a sorted list of each
+     * statement's signature, which is itself a concatenation of the string form
+     * of all grounded elements.
+     *
+     * Nodes other than the given node are considered grounded if they are a
+     * member in the given hash.
+     *
+     * Returns a tuple consisting of grounded being true or false and the string
+     * for the hash
+     *
+     * @ignore
+     */
+    private static function nodeHashFor($node, $statements, $hashes)
+    {
+        $statement_signatures = array();
+        foreach ($statements as $statement) {
+            foreach ($statement as $n) {
+                if ($n['type'] != 'literal' and $n['value'] == $node) {
+                    array_push(
+                        $statement_signatures,
+                        self::hashStringFor($statement, $hashes, $node)
+                    );
+                }
+            }
+        }
+
+        // Note that we sort the signatures--without a canonical ordering,
+        // we might get different hashes for equivalent nodes
+        sort($statement_signatures);
+
+        // Convert statements into one long string and hash it
+        return sha1(implode('', $statement_signatures));
+    }
+
+    /**
+     * Returns true if a given node is grounded
+     * A node is groundd if it is not a blank node or it is included
+     * in the given mapping of grounded nodes.
+     *
+     * @ignore
+     */
+    private static function nodeIsGrounded($node, $statements, $hashes)
+    {
+        $grounded = true;
+        foreach ($statements as $statement) {
+            if (in_array($node, $statement)) {
+                foreach ($statement as $resource) {
+                    if ($node['type'] != 'bnode' or
+                        isset($hashes[$node['value']]) or
+                        $resource == $node
+                    ) {
+                        $grounded = false;
+                    }
+                }
+            }
+        }
+        return $grounded;
+    }
+
+    /**
+     * Provide a string signature for the given statement, collecting
+     * string signatures for grounded node elements.
+     *
+     * @ignore
+     */
+    private static function hashStringFor($statement, $hashes, $node)
+    {
+        $str = "";
+        foreach ($statement as $r) {
+            $str .= self::stringForNode($r, $hashes, $node);
+        }
+        return $str;
+    }
+
+    /**
+     * Provides a string for the given node for use in a string signature
+     * Non-anonymous nodes will return their string form.  Grounded anonymous
+     * nodes will return their hashed form.
+     *
+     * @ignore
+     */
+    private static function stringForNode($node, $hashes, $target)
+    {
+        if (is_null($node)) {
+            return "";
+        } elseif ($node['type'] == 'bnode') {
+            if ($node['value'] == $target) {
+                return "itself";
+            } elseif (isset($hashes[$node['value']])) {
+                return $hashes[$node['value']];
+            } else {
+                return "a blank node";
+            }
+        } else {
+            $s = new EasyRdf_Serialiser_Ntriples();
+            return $s->serialiseValue($node);
+        }
+    }
+}