|  | /*
  ==============================================================================
   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-11 by Raw Material Software Ltd.
  ------------------------------------------------------------------------------
   JUCE can be redistributed and/or modified under the terms of the GNU General
   Public License (Version 2), as published by the Free Software Foundation.
   A copy of the license is included in the JUCE distribution, or can be found
   online at www.gnu.org/licenses.
   JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
  ------------------------------------------------------------------------------
   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.rawmaterialsoftware.com/juce for more information.
  ==============================================================================
*/
struct TextDiffHelpers
{
    enum { minLengthToMatch = 3 };
    struct StringRegion
    {
        StringRegion (const String& s) noexcept
            : text (s.getCharPointer()), start (0), length (s.length()) {}
        StringRegion (const String::CharPointerType& t, int s, int len)  noexcept
            : text (t), start (s), length (len) {}
        String::CharPointerType text;
        int start, length;
    };
    static void addInsertion (TextDiff& td, const String::CharPointerType& text, int index, int length)
    {
        TextDiff::Change c;
        c.insertedText = String (text, (size_t) length);
        c.start = index;
        c.length = length;
        td.changes.add (c);
    }
    static void addDeletion (TextDiff& td, int index, int length)
    {
        TextDiff::Change c;
        c.start = index;
        c.length = length;
        td.changes.add (c);
    }
    static void diffSkippingCommonStart (TextDiff& td, const StringRegion& a, const StringRegion& b)
    {
        String::CharPointerType sa (a.text);
        String::CharPointerType sb (b.text);
        const int maxLen = jmax (a.length, b.length);
        for (int i = 0; i < maxLen; ++i, ++sa, ++sb)
        {
            if (*sa != *sb)
            {
                diffRecursively (td, StringRegion (sa, a.start + i, a.length - i),
                                     StringRegion (sb, b.start + i, b.length - i));
                break;
            }
        }
    }
    static void diffRecursively (TextDiff& td, const StringRegion& a, const StringRegion& b)
    {
        int indexA, indexB;
        const int len = findLongestCommonSubstring (a.text, a.length,
                                                    b.text, b.length,
                                                    indexA, indexB);
        if (len >= minLengthToMatch)
        {
            jassert (indexA >= 0 && indexA <= a.length);
            jassert (indexB >= 0 && indexB <= b.length);
            jassert (String (a.text + indexA, (size_t) len) == String (b.text + indexB, (size_t) len));
            if (indexA > 0 && indexB > 0)
                diffSkippingCommonStart (td, StringRegion (a.text, a.start, indexA),
                                             StringRegion (b.text, b.start, indexB));
            else if (indexA > 0)
                addDeletion (td, b.start, indexA);
            else if (indexB > 0)
                addInsertion (td, b.text, b.start, indexB);
            diffRecursively (td, StringRegion (a.text + indexA + len, a.start + indexA + len, a.length - indexA - len),
                                 StringRegion (b.text + indexB + len, b.start + indexB + len, b.length - indexB - len));
        }
        else
        {
            if (a.length > 0)   addDeletion (td, b.start, a.length);
            if (b.length > 0)   addInsertion (td, b.text, b.start, b.length);
        }
    }
    static int findLongestCommonSubstring (String::CharPointerType a, const int lenA,
                                           const String::CharPointerType& b, const int lenB,
                                           int& indexInA, int& indexInB)
    {
        if (lenA == 0 || lenB == 0)
            return 0;
        HeapBlock<int> lines;
        lines.calloc (2 + 2 * (size_t) lenB);
        int* l0 = lines;
        int* l1 = l0 + lenB + 1;
        int bestLength = 0;
        indexInA = indexInB = 0;
        for (int i = 0; i < lenA; ++i)
        {
            const juce_wchar ca = a.getAndAdvance();
            String::CharPointerType b2 (b);
            for (int j = 0; j < lenB; ++j)
            {
                if (ca != b2.getAndAdvance())
                {
                    l1[j + 1] = 0;
                }
                else
                {
                    const int len = l0[j] + 1;
                    l1[j + 1] = len;
                    if (len > bestLength)
                    {
                        bestLength = len;
                        indexInA = i;
                        indexInB = j;
                    }
                }
            }
            std::swap (l0, l1);
        }
        indexInA -= bestLength - 1;
        indexInB -= bestLength - 1;
        return bestLength;
    }
};
TextDiff::TextDiff (const String& original, const String& target)
{
    TextDiffHelpers::diffSkippingCommonStart (*this, original, target);
}
String TextDiff::appliedTo (String text) const
{
    for (int i = 0; i < changes.size(); ++i)
        text = changes.getReference(i).appliedTo (text);
    return text;
}
bool TextDiff::Change::isDeletion() const noexcept
{
    return insertedText.isEmpty();
}
String TextDiff::Change::appliedTo (const String& text) const noexcept
{
    return text.substring (0, start) + (isDeletion() ? text.substring (start + length)
                                                     : (insertedText + text.substring (start)));
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class DiffTests  : public UnitTest
{
public:
    DiffTests() : UnitTest ("TextDiff class") {}
    static String createString()
    {
        juce_wchar buffer[50] = { 0 };
        Random r;
        for (int i = r.nextInt (49); --i >= 0;)
        {
            if (r.nextInt (10) == 0)
            {
                do
                {
                    buffer[i] = (juce_wchar) (1 + r.nextInt (0x10ffff - 1));
                }
                while (! CharPointer_UTF16::canRepresent (buffer[i]));
            }
            else
                buffer[i] = (juce_wchar) ('a' + r.nextInt (3));
        }
        return CharPointer_UTF32 (buffer);
    }
    void testDiff (const String& a, const String& b)
    {
        TextDiff diff (a, b);
        const String result (diff.appliedTo (a));
        expectEquals (result, b);
    }
    void runTest()
    {
        beginTest ("TextDiff");
        testDiff (String::empty, String::empty);
        testDiff ("x", String::empty);
        testDiff (String::empty, "x");
        testDiff ("x", "x");
        testDiff ("x", "y");
        testDiff ("xxx", "x");
        testDiff ("x", "xxx");
        for (int i = 5000; --i >= 0;)
        {
            String s (createString());
            testDiff (s, createString());
            testDiff (s + createString(), s + createString());
        }
    }
};
static DiffTests diffTests;
#endif
 |