├── .gitattributes ├── .github └── workflows │ └── buildandtest.yml ├── .gitignore ├── DiffMatchPatch.PerformanceTest ├── DiffMatchPatch.PerformanceTest.csproj ├── PerformanceTest.cs ├── Speedtest1.txt ├── Speedtest2.txt ├── left.txt └── right.txt ├── DiffMatchPatch.Tests ├── AssemblyInfo.cs ├── BitapAlgorithmTests.cs ├── DiffListTests.cs ├── DiffList_CharsToLinesTests.cs ├── DiffList_CleanupEfficiencyTests.cs ├── DiffList_CleanupMergeTests.cs ├── DiffList_CleanupSemanticLosslessTests.cs ├── DiffList_CleanupSemanticTests.cs ├── DiffList_ToDeltaTests.cs ├── DiffMatchPatch.Original.cs ├── DiffMatchPatch.Tests.csproj ├── DiffMatchPatchTest.Original.cs ├── Diff_ComputeTests.cs ├── HalfMatchResultTests.cs ├── OriginalTests.cs ├── PatchTests.cs ├── TextUtil_CommonOverlapTests.cs ├── TextUtil_HalfMatchTests.cs ├── TextUtil_LinesToCharsTests.cs └── TextUtil_MatchPatternTests.cs ├── DiffMatchPatch.lutconfig ├── DiffMatchPatch.sln ├── DiffMatchPatch ├── BitapAlgorithm.cs ├── Constants.cs ├── Diff.cs ├── DiffAlgorithm.cs ├── DiffList.cs ├── DiffListBuilder.cs ├── DiffMatchPatch.csproj ├── Extensions.cs ├── HalfMatchResult.cs ├── ImmutableListWithValueSemantics.cs ├── IsExternalInit.cs ├── LineToCharCompressor.cs ├── MatchSettings.cs ├── Operation.cs ├── Patch.cs ├── PatchList.cs ├── PatchSettings.cs ├── Properties │ ├── AssemblyInfo.cs │ └── Usings.cs └── TextUtil.cs ├── LICENSE ├── README.md └── TestConsole ├── Program.cs └── TestConsole.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/buildandtest.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | .vs/ 19 | 20 | # Roslyn cache directories 21 | *.ide/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | #NUNIT 28 | *.VisualState.xml 29 | TestResult.xml 30 | 31 | # Build Results of an ATL Project 32 | [Dd]ebugPS/ 33 | [Rr]eleasePS/ 34 | dlldata.c 35 | 36 | *_i.c 37 | *_p.c 38 | *_i.h 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.svclog 59 | *.scc 60 | 61 | # Chutzpah Test files 62 | _Chutzpah* 63 | 64 | # Visual C++ cache files 65 | ipch/ 66 | *.aps 67 | *.ncb 68 | *.opensdf 69 | *.sdf 70 | *.cachefile 71 | 72 | # Visual Studio profiler 73 | *.psess 74 | *.vsp 75 | *.vspx 76 | 77 | # TFS 2012 Local Workspace 78 | $tf/ 79 | 80 | # Guidance Automation Toolkit 81 | *.gpState 82 | 83 | # ReSharper is a .NET coding add-in 84 | _ReSharper*/ 85 | *.[Rr]e[Ss]harper 86 | *.DotSettings.user 87 | 88 | # JustCode is a .NET coding addin-in 89 | .JustCode 90 | 91 | # TeamCity is a build add-in 92 | _TeamCity* 93 | 94 | # DotCover is a Code Coverage Tool 95 | *.dotCover 96 | 97 | # NCrunch 98 | _NCrunch_* 99 | .*crunch*.local.xml 100 | 101 | # MightyMoose 102 | *.mm.* 103 | AutoTest.Net/ 104 | 105 | # Web workbench (sass) 106 | .sass-cache/ 107 | 108 | # Installshield output folder 109 | [Ee]xpress/ 110 | 111 | # DocProject is a documentation generator add-in 112 | DocProject/buildhelp/ 113 | DocProject/Help/*.HxT 114 | DocProject/Help/*.HxC 115 | DocProject/Help/*.hhc 116 | DocProject/Help/*.hhk 117 | DocProject/Help/*.hhp 118 | DocProject/Help/Html2 119 | DocProject/Help/html 120 | 121 | # Click-Once directory 122 | publish/ 123 | 124 | # Publish Web Output 125 | *.[Pp]ublish.xml 126 | *.azurePubxml 127 | ## TODO: Comment the next line if you want to checkin your 128 | ## web deploy settings but do note that will include unencrypted 129 | ## passwords 130 | #*.pubxml 131 | 132 | # NuGet Packages Directory 133 | packages/* 134 | ## TODO: If the tool you use requires repositories.config 135 | ## uncomment the next line 136 | #!packages/repositories.config 137 | 138 | # Enable "build/" folder in the NuGet Packages folder since 139 | # NuGet packages use it for MSBuild targets. 140 | # This line needs to be after the ignore of the build folder 141 | # (and the packages folder if the line above has been uncommented) 142 | !packages/build/ 143 | 144 | # Windows Azure Build Output 145 | csx/ 146 | *.build.csdef 147 | 148 | # Windows Store app package directory 149 | AppPackages/ 150 | 151 | # Others 152 | sql/ 153 | *.Cache 154 | ClientBin/ 155 | [Ss]tyle[Cc]op.* 156 | ~$* 157 | *~ 158 | *.dbmdl 159 | *.dbproj.schemaview 160 | *.pfx 161 | *.publishsettings 162 | node_modules/ 163 | 164 | # RIA/Silverlight projects 165 | Generated_Code/ 166 | 167 | # Backup & report files from converting an old project file 168 | # to a newer Visual Studio version. Backup files are not needed, 169 | # because we have git ;-) 170 | _UpgradeReport_Files/ 171 | Backup*/ 172 | UpgradeLog*.XML 173 | UpgradeLog*.htm 174 | 175 | # SQL Server files 176 | *.mdf 177 | *.ldf 178 | 179 | # Business Intelligence projects 180 | *.rdl.data 181 | *.bim.layout 182 | *.bim_*.settings 183 | 184 | # Microsoft Fakes 185 | FakesAssemblies/ 186 | 187 | # LightSwitch generated files 188 | GeneratedArtifacts/ 189 | _Pvt_Extensions/ 190 | ModelManifest.xml -------------------------------------------------------------------------------- /DiffMatchPatch.PerformanceTest/DiffMatchPatch.PerformanceTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | preview 6 | 7 | 8 | 9 | 10 | PreserveNewest 11 | 12 | 13 | PreserveNewest 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /DiffMatchPatch.PerformanceTest/PerformanceTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using Xunit; 5 | 6 | namespace DiffMatchPatch.PerformanceTest 7 | { 8 | public class PerformanceTest 9 | { 10 | //public static void Main() 11 | //{ 12 | // var t = new PerformanceTest(); 13 | // t.TestPerformance1(); 14 | //} 15 | 16 | [Fact] 17 | public void TestPerformance1() 18 | { 19 | var oldText = File.ReadAllText("left.txt"); 20 | var newText = File.ReadAllText("right.txt"); 21 | 22 | 23 | var sw = Stopwatch.StartNew(); 24 | for (int i = 0; i < 1; i++) 25 | { 26 | var diff = Diff.Compute(oldText, newText, 5); 27 | diff.CleanupEfficiency(); 28 | diff.CleanupSemantic(); 29 | } 30 | //var patched = Patch.FromDiffs(diff).Apply(oldText); 31 | var elapsed = sw.Elapsed; 32 | Console.WriteLine(elapsed); 33 | //var fileName = Path.ChangeExtension(Path.GetTempFileName(), "html"); 34 | //File.WriteAllText(fileName, diff.PrettyHtml()); 35 | //Process.Start(fileName); 36 | } 37 | 38 | [Fact] 39 | public void TestPerformance2() 40 | { 41 | string text1 = File.ReadAllText("Speedtest1.txt"); 42 | string text2 = File.ReadAllText("Speedtest2.txt"); 43 | 44 | 45 | // Execute one reverse diff as a warmup. 46 | Diff.Compute(text2, text1); 47 | GC.Collect(); 48 | GC.WaitForPendingFinalizers(); 49 | 50 | var sw = Stopwatch.StartNew(); 51 | var diff = Diff.Compute(text1, text2); 52 | Console.WriteLine("Elapsed time: " + sw.Elapsed); 53 | //var fileName = Path.ChangeExtension(Path.GetTempFileName(), "html"); 54 | //File.WriteAllText(fileName, diff.PrettyHtml()); 55 | //Process.Start(fileName); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DiffMatchPatch.PerformanceTest/Speedtest1.txt: -------------------------------------------------------------------------------- 1 | This is a '''list of newspapers published by [[Journal Register Company]]'''. 2 | 3 | The company owns daily and weekly newspapers, other print media properties and newspaper-affiliated local Websites in the [[U.S.]] states of [[Connecticut]], [[Michigan]], [[New York]], [[Ohio]] and [[Pennsylvania]], organized in six geographic "clusters":[http://www.journalregister.com/newspapers.html Journal Register Company: Our Newspapers], accessed February 10, 2008. 4 | 5 | == Capital-Saratoga == 6 | Three dailies, associated weeklies and [[pennysaver]]s in greater [[Albany, New York]]; also [http://www.capitalcentral.com capitalcentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. 7 | 8 | * ''The Oneida Daily Dispatch'' {{WS|oneidadispatch.com}} of [[Oneida, New York]] 9 | * ''[[The Record (Troy)|The Record]]'' {{WS|troyrecord.com}} of [[Troy, New York]] 10 | * ''[[The Saratogian]]'' {{WS|saratogian.com}} of [[Saratoga Springs, New York]] 11 | * Weeklies: 12 | ** ''Community News'' {{WS|cnweekly.com}} weekly of [[Clifton Park, New York]] 13 | ** ''Rome Observer'' of [[Rome, New York]] 14 | ** ''Life & Times of Utica'' of [[Utica, New York]] 15 | 16 | == Connecticut == 17 | Five dailies, associated weeklies and [[pennysaver]]s in the state of [[Connecticut]]; also [http://www.ctcentral.com CTcentral.com], [http://www.ctcarsandtrucks.com CTCarsAndTrucks.com] and [http://www.jobsinct.com JobsInCT.com]. 18 | 19 | * ''The Middletown Press'' {{WS|middletownpress.com}} of [[Middletown, Connecticut|Middletown]] 20 | * ''[[New Haven Register]]'' {{WS|newhavenregister.com}} of [[New Haven, Connecticut|New Haven]] 21 | * ''The Register Citizen'' {{WS|registercitizen.com}} of [[Torrington, Connecticut|Torrington]] 22 | 23 | * [[New Haven Register#Competitors|Elm City Newspapers]] {{WS|ctcentral.com}} 24 | ** ''The Advertiser'' of [[East Haven, Connecticut|East Haven]] 25 | ** ''Hamden Chronicle'' of [[Hamden, Connecticut|Hamden]] 26 | ** ''Milford Weekly'' of [[Milford, Connecticut|Milford]] 27 | ** ''The Orange Bulletin'' of [[Orange, Connecticut|Orange]] 28 | ** ''The Post'' of [[North Haven, Connecticut|North Haven]] 29 | ** ''Shelton Weekly'' of [[Shelton, Connecticut|Shelton]] 30 | ** ''The Stratford Bard'' of [[Stratford, Connecticut|Stratford]] 31 | ** ''Wallingford Voice'' of [[Wallingford, Connecticut|Wallingford]] 32 | ** ''West Haven News'' of [[West Haven, Connecticut|West Haven]] 33 | * Housatonic Publications 34 | ** ''The New Milford Times'' {{WS|newmilfordtimes.com}} of [[New Milford, Connecticut|New Milford]] 35 | ** ''The Brookfield Journal'' of [[Brookfield, Connecticut|Brookfield]] 36 | ** ''The Kent Good Times Dispatch'' of [[Kent, Connecticut|Kent]] 37 | ** ''The Bethel Beacon'' of [[Bethel, Connecticut|Bethel]] 38 | ** ''The Litchfield Enquirer'' of [[Litchfield, Connecticut|Litchfield]] 39 | ** ''Litchfield County Times'' of [[Litchfield, Connecticut|Litchfield]] 40 | * Imprint Newspapers {{WS|imprintnewspapers.com}} 41 | ** ''West Hartford News'' of [[West Hartford, Connecticut|West Hartford]] 42 | ** ''Windsor Journal'' of [[Windsor, Connecticut|Windsor]] 43 | ** ''Windsor Locks Journal'' of [[Windsor Locks, Connecticut|Windsor Locks]] 44 | ** ''Avon Post'' of [[Avon, Connecticut|Avon]] 45 | ** ''Farmington Post'' of [[Farmington, Connecticut|Farmington]] 46 | ** ''Simsbury Post'' of [[Simsbury, Connecticut|Simsbury]] 47 | ** ''Tri-Town Post'' of [[Burlington, Connecticut|Burlington]], [[Canton, Connecticut|Canton]] and [[Harwinton, Connecticut|Harwinton]] 48 | * Minuteman Publications 49 | ** ''[[Fairfield Minuteman]]'' of [[Fairfield, Connecticut|Fairfield]] 50 | ** ''The Westport Minuteman'' {{WS|westportminuteman.com}} of [[Westport, Connecticut|Westport]] 51 | * Shoreline Newspapers weeklies: 52 | ** ''Branford Review'' of [[Branford, Connecticut|Branford]] 53 | ** ''Clinton Recorder'' of [[Clinton, Connecticut|Clinton]] 54 | ** ''The Dolphin'' of [[Naval Submarine Base New London]] in [[New London, Connecticut|New London]] 55 | ** ''Main Street News'' {{WS|ctmainstreetnews.com}} of [[Essex, Connecticut|Essex]] 56 | ** ''Pictorial Gazette'' of [[Old Saybrook, Connecticut|Old Saybrook]] 57 | ** ''Regional Express'' of [[Colchester, Connecticut|Colchester]] 58 | ** ''Regional Standard'' of [[Colchester, Connecticut|Colchester]] 59 | ** ''Shoreline Times'' {{WS|shorelinetimes.com}} of [[Guilford, Connecticut|Guilford]] 60 | ** ''Shore View East'' of [[Madison, Connecticut|Madison]] 61 | ** ''Shore View West'' of [[Guilford, Connecticut|Guilford]] 62 | * Other weeklies: 63 | ** ''Registro'' {{WS|registroct.com}} of [[New Haven, Connecticut|New Haven]] 64 | ** ''Thomaston Express'' {{WS|thomastownexpress.com}} of [[Thomaston, Connecticut|Thomaston]] 65 | ** ''Foothills Traders'' {{WS|foothillstrader.com}} of Torrington, Bristol, Canton 66 | 67 | == Michigan == 68 | Four dailies, associated weeklies and [[pennysaver]]s in the state of [[Michigan]]; also [http://www.micentralhomes.com MIcentralhomes.com] and [http://www.micentralautos.com MIcentralautos.com] 69 | * ''[[Oakland Press]]'' {{WS|theoaklandpress.com}} of [[Oakland, Michigan|Oakland]] 70 | * ''Daily Tribune'' {{WS|dailytribune.com}} of [[Royal Oak, Michigan|Royal Oak]] 71 | * ''Macomb Daily'' {{WS|macombdaily.com}} of [[Mt. Clemens, Michigan|Mt. Clemens]] 72 | * ''[[Morning Sun]]'' {{WS|themorningsun.com}} of [[Mount Pleasant, Michigan|Mount Pleasant]] 73 | * Heritage Newspapers {{WS|heritage.com}} 74 | ** ''Belleville View'' 75 | ** ''Ile Camera'' 76 | ** ''Monroe Guardian'' 77 | ** ''Ypsilanti Courier'' 78 | ** ''News-Herald'' 79 | ** ''Press & Guide'' 80 | ** ''Chelsea Standard & Dexter Leader'' 81 | ** ''Manchester Enterprise'' 82 | ** ''Milan News-Leader'' 83 | ** ''Saline Reporter'' 84 | * Independent Newspapers {{WS|sourcenewspapers.com}} 85 | ** ''Advisor'' 86 | ** ''Source'' 87 | * Morning Star {{WS|morningstarpublishing.com}} 88 | ** ''Alma Reminder'' 89 | ** ''Alpena Star'' 90 | ** ''Antrim County News'' 91 | ** ''Carson City Reminder'' 92 | ** ''The Leader & Kalkaskian'' 93 | ** ''Ogemaw/Oscoda County Star'' 94 | ** ''Petoskey/Charlevoix Star'' 95 | ** ''Presque Isle Star'' 96 | ** ''Preview Community Weekly'' 97 | ** ''Roscommon County Star'' 98 | ** ''St. Johns Reminder'' 99 | ** ''Straits Area Star'' 100 | ** ''The (Edmore) Advertiser'' 101 | * Voice Newspapers {{WS|voicenews.com}} 102 | ** ''Armada Times'' 103 | ** ''Bay Voice'' 104 | ** ''Blue Water Voice'' 105 | ** ''Downriver Voice'' 106 | ** ''Macomb Township Voice'' 107 | ** ''North Macomb Voice'' 108 | ** ''Weekend Voice'' 109 | ** ''Suburban Lifestyles'' {{WS|suburbanlifestyles.com}} 110 | 111 | == Mid-Hudson == 112 | One daily, associated magazines in the [[Hudson River Valley]] of [[New York]]; also [http://www.midhudsoncentral.com MidHudsonCentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. 113 | 114 | * ''[[Daily Freeman]]'' {{WS|dailyfreeman.com}} of [[Kingston, New York]] 115 | 116 | == Ohio == 117 | Two dailies, associated magazines and three shared Websites, all in the state of [[Ohio]]: [http://www.allaroundcleveland.com AllAroundCleveland.com], [http://www.allaroundclevelandcars.com AllAroundClevelandCars.com] and [http://www.allaroundclevelandjobs.com AllAroundClevelandJobs.com]. 118 | 119 | * ''[[The News-Herald (Ohio)|The News-Herald]]'' {{WS|news-herald.com}} of [[Willoughby, Ohio|Willoughby]] 120 | * ''[[The Morning Journal]]'' {{WS|morningjournal.com}} of [[Lorain, Ohio|Lorain]] 121 | 122 | == Philadelphia area == 123 | Seven dailies and associated weeklies and magazines in [[Pennsylvania]] and [[New Jersey]], and associated Websites: [http://www.allaroundphilly.com AllAroundPhilly.com], [http://www.jobsinnj.com JobsInNJ.com], [http://www.jobsinpa.com JobsInPA.com], and [http://www.phillycarsearch.com PhillyCarSearch.com]. 124 | 125 | * ''The Daily Local'' {{WS|dailylocal.com}} of [[West Chester, Pennsylvania|West Chester]] 126 | * ''[[Delaware County Daily and Sunday Times]] {{WS|delcotimes.com}} of Primos 127 | * ''[[The Mercury (Pennsylvania)|The Mercury]]'' {{WS|pottstownmercury.com}} of [[Pottstown, Pennsylvania|Pottstown]] 128 | * ''The Phoenix'' {{WS|phoenixvillenews.com}} of [[Phoenixville, Pennsylvania|Phoenixville]] 129 | * ''[[The Reporter (Lansdale)|The Reporter]]'' {{WS|thereporteronline.com}} of [[Lansdale, Pennsylvania|Lansdale]] 130 | * ''The Times Herald'' {{WS|timesherald.com}} of [[Norristown, Pennsylvania|Norristown]] 131 | * ''[[The Trentonian]]'' {{WS|trentonian.com}} of [[Trenton, New Jersey]] 132 | 133 | * Weeklies 134 | ** ''El Latino Expreso'' of [[Trenton, New Jersey]] 135 | ** ''La Voz'' of [[Norristown, Pennsylvania]] 136 | ** ''The Village News'' of [[Downingtown, Pennsylvania]] 137 | ** ''The Times Record'' of [[Kennett Square, Pennsylvania]] 138 | ** ''The Tri-County Record'' {{WS|tricountyrecord.com}} of [[Morgantown, Pennsylvania]] 139 | ** ''News of Delaware County'' {{WS|newsofdelawarecounty.com}}of [[Havertown, Pennsylvania]] 140 | ** ''Main Line Times'' {{WS|mainlinetimes.com}}of [[Ardmore, Pennsylvania]] 141 | ** ''Penny Pincher'' of [[Pottstown, Pennsylvania]] 142 | ** ''Town Talk'' {{WS|towntalknews.com}} of [[Ridley, Pennsylvania]] 143 | * Chesapeake Publishing {{WS|pa8newsgroup.com}} 144 | ** ''Solanco Sun Ledger'' of [[Quarryville, Pennsylvania]] 145 | ** ''Columbia Ledger'' of [[Columbia, Pennsylvania]] 146 | ** ''Coatesville Ledger'' of [[Downingtown, Pennsylvania]] 147 | ** ''Parkesburg Post Ledger'' of [[Quarryville, Pennsylvania]] 148 | ** ''Downingtown Ledger'' of [[Downingtown, Pennsylvania]] 149 | ** ''The Kennett Paper'' of [[Kennett Square, Pennsylvania]] 150 | ** ''Avon Grove Sun'' of [[West Grove, Pennsylvania]] 151 | ** ''Oxford Tribune'' of [[Oxford, Pennsylvania]] 152 | ** ''Elizabethtown Chronicle'' of [[Elizabethtown, Pennsylvania]] 153 | ** ''Donegal Ledger'' of [[Donegal, Pennsylvania]] 154 | ** ''Chadds Ford Post'' of [[Chadds Ford, Pennsylvania]] 155 | ** ''The Central Record'' of [[Medford, New Jersey]] 156 | ** ''Maple Shade Progress'' of [[Maple Shade, New Jersey]] 157 | * Intercounty Newspapers {{WS|buckslocalnews.com}} 158 | ** ''The Review'' of Roxborough, Pennsylvania 159 | ** ''The Recorder'' of [[Conshohocken, Pennsylvania]] 160 | ** ''The Leader'' of [[Mount Airy, Pennsylvania|Mount Airy]] and West Oak Lake, Pennsylvania 161 | ** ''The Pennington Post'' of [[Pennington, New Jersey]] 162 | ** ''The Bristol Pilot'' of [[Bristol, Pennsylvania]] 163 | ** ''Yardley News'' of [[Yardley, Pennsylvania]] 164 | ** ''New Hope Gazette'' of [[New Hope, Pennsylvania]] 165 | ** ''Doylestown Patriot'' of [[Doylestown, Pennsylvania]] 166 | ** ''Newtown Advance'' of [[Newtown, Pennsylvania]] 167 | ** ''The Plain Dealer'' of [[Williamstown, New Jersey]] 168 | ** ''News Report'' of [[Sewell, New Jersey]] 169 | ** ''Record Breeze'' of [[Berlin, New Jersey]] 170 | ** ''Newsweekly'' of [[Moorestown, New Jersey]] 171 | ** ''Haddon Herald'' of [[Haddonfield, New Jersey]] 172 | ** ''New Egypt Press'' of [[New Egypt, New Jersey]] 173 | ** ''Community News'' of [[Pemberton, New Jersey]] 174 | ** ''Plymouth Meeting Journal'' of [[Plymouth Meeting, Pennsylvania]] 175 | ** ''Lafayette Hill Journal'' of [[Lafayette Hill, Pennsylvania]] 176 | * Montgomery Newspapers {{WS|montgomerynews.com}} 177 | ** ''Ambler Gazette'' of [[Ambler, Pennsylvania]] 178 | ** ''Central Bucks Life'' of [[Bucks County, Pennsylvania]] 179 | ** ''The Colonial'' of [[Plymouth Meeting, Pennsylvania]] 180 | ** ''Glenside News'' of [[Glenside, Pennsylvania]] 181 | ** ''The Globe'' of [[Lower Moreland Township, Pennsylvania]] 182 | ** ''Main Line Life'' of [[Ardmore, Pennsylvania]] 183 | ** ''Montgomery Life'' of [[Fort Washington, Pennsylvania]] 184 | ** ''North Penn Life'' of [[Lansdale, Pennsylvania]] 185 | ** ''Perkasie News Herald'' of [[Perkasie, Pennsylvania]] 186 | ** ''Public Spirit'' of [[Hatboro, Pennsylvania]] 187 | ** ''Souderton Independent'' of [[Souderton, Pennsylvania]] 188 | ** ''Springfield Sun'' of [[Springfield, Pennsylvania]] 189 | ** ''Spring-Ford Reporter'' of [[Royersford, Pennsylvania]] 190 | ** ''Times Chronicle'' of [[Jenkintown, Pennsylvania]] 191 | ** ''Valley Item'' of [[Perkiomenville, Pennsylvania]] 192 | ** ''Willow Grove Guide'' of [[Willow Grove, Pennsylvania]] 193 | * News Gleaner Publications (closed December 2008) {{WS|newsgleaner.com}} 194 | ** ''Life Newspapers'' of [[Philadelphia, Pennsylvania]] 195 | * Suburban Publications 196 | ** ''The Suburban & Wayne Times'' {{WS|waynesuburban.com}} of [[Wayne, Pennsylvania]] 197 | ** ''The Suburban Advertiser'' of [[Exton, Pennsylvania]] 198 | ** ''The King of Prussia Courier'' of [[King of Prussia, Pennsylvania]] 199 | * Press Newspapers {{WS|countypressonline.com}} 200 | ** ''County Press'' of [[Newtown Square, Pennsylvania]] 201 | ** ''Garnet Valley Press'' of [[Glen Mills, Pennsylvania]] 202 | ** ''Haverford Press'' of [[Newtown Square, Pennsylvania]] (closed January 2009) 203 | ** ''Hometown Press'' of [[Glen Mills, Pennsylvania]] (closed January 2009) 204 | ** ''Media Press'' of [[Newtown Square, Pennsylvania]] (closed January 2009) 205 | ** ''Springfield Press'' of [[Springfield, Pennsylvania]] 206 | * Berks-Mont Newspapers {{WS|berksmontnews.com}} 207 | ** ''The Boyertown Area Times'' of [[Boyertown, Pennsylvania]] 208 | ** ''The Kutztown Area Patriot'' of [[Kutztown, Pennsylvania]] 209 | ** ''The Hamburg Area Item'' of [[Hamburg, Pennsylvania]] 210 | ** ''The Southern Berks News'' of [[Exeter Township, Berks County, Pennsylvania]] 211 | ** ''The Free Press'' of [[Quakertown, Pennsylvania]] 212 | ** ''The Saucon News'' of [[Quakertown, Pennsylvania]] 213 | ** ''Westside Weekly'' of [[Reading, Pennsylvania]] 214 | 215 | * Magazines 216 | ** ''Bucks Co. Town & Country Living'' 217 | ** ''Chester Co. Town & Country Living'' 218 | ** ''Montomgery Co. Town & Country Living'' 219 | ** ''Garden State Town & Country Living'' 220 | ** ''Montgomery Homes'' 221 | ** ''Philadelphia Golfer'' 222 | ** ''Parents Express'' 223 | ** ''Art Matters'' 224 | 225 | {{JRC}} 226 | 227 | ==References== 228 | 229 | 230 | [[Category:Journal Register publications|*]] 231 | -------------------------------------------------------------------------------- /DiffMatchPatch.PerformanceTest/Speedtest2.txt: -------------------------------------------------------------------------------- 1 | This is a '''list of newspapers published by [[Journal Register Company]]'''. 2 | 3 | The company owns daily and weekly newspapers, other print media properties and newspaper-affiliated local Websites in the [[U.S.]] states of [[Connecticut]], [[Michigan]], [[New York]], [[Ohio]], [[Pennsylvania]] and [[New Jersey]], organized in six geographic "clusters":[http://www.journalregister.com/publications.html Journal Register Company: Our Publications], accessed April 21, 2010. 4 | 5 | == Capital-Saratoga == 6 | Three dailies, associated weeklies and [[pennysaver]]s in greater [[Albany, New York]]; also [http://www.capitalcentral.com capitalcentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. 7 | 8 | * ''The Oneida Daily Dispatch'' {{WS|oneidadispatch.com}} of [[Oneida, New York]] 9 | * ''[[The Record (Troy)|The Record]]'' {{WS|troyrecord.com}} of [[Troy, New York]] 10 | * ''[[The Saratogian]]'' {{WS|saratogian.com}} of [[Saratoga Springs, New York]] 11 | * Weeklies: 12 | ** ''Community News'' {{WS|cnweekly.com}} weekly of [[Clifton Park, New York]] 13 | ** ''Rome Observer'' {{WS|romeobserver.com}} of [[Rome, New York]] 14 | ** ''WG Life '' {{WS|saratogian.com/wglife/}} of [[Wilton, New York]] 15 | ** ''Ballston Spa Life '' {{WS|saratogian.com/bspalife}} of [[Ballston Spa, New York]] 16 | ** ''Greenbush Life'' {{WS|troyrecord.com/greenbush}} of [[Troy, New York]] 17 | ** ''Latham Life'' {{WS|troyrecord.com/latham}} of [[Latham, New York]] 18 | ** ''River Life'' {{WS|troyrecord.com/river}} of [[Troy, New York]] 19 | 20 | == Connecticut == 21 | Three dailies, associated weeklies and [[pennysaver]]s in the state of [[Connecticut]]; also [http://www.ctcentral.com CTcentral.com], [http://www.ctcarsandtrucks.com CTCarsAndTrucks.com] and [http://www.jobsinct.com JobsInCT.com]. 22 | 23 | * ''The Middletown Press'' {{WS|middletownpress.com}} of [[Middletown, Connecticut|Middletown]] 24 | * ''[[New Haven Register]]'' {{WS|newhavenregister.com}} of [[New Haven, Connecticut|New Haven]] 25 | * ''The Register Citizen'' {{WS|registercitizen.com}} of [[Torrington, Connecticut|Torrington]] 26 | 27 | * Housatonic Publications 28 | ** ''The Housatonic Times'' {{WS|housatonictimes.com}} of [[New Milford, Connecticut|New Milford]] 29 | ** ''Litchfield County Times'' {{WS|countytimes.com}} of [[Litchfield, Connecticut|Litchfield]] 30 | 31 | * Minuteman Publications 32 | ** ''[[Fairfield Minuteman]]'' {{WS|fairfieldminuteman.com}}of [[Fairfield, Connecticut|Fairfield]] 33 | ** ''The Westport Minuteman'' {{WS|westportminuteman.com}} of [[Westport, Connecticut|Westport]] 34 | 35 | * Shoreline Newspapers 36 | ** ''The Dolphin'' {{WS|dolphin-news.com}} of [[Naval Submarine Base New London]] in [[New London, Connecticut|New London]] 37 | ** ''Shoreline Times'' {{WS|shorelinetimes.com}} of [[Guilford, Connecticut|Guilford]] 38 | 39 | * Foothills Media Group {{WS|foothillsmediagroup.com}} 40 | ** ''Thomaston Express'' {{WS|thomastonexpress.com}} of [[Thomaston, Connecticut|Thomaston]] 41 | ** ''Good News About Torrington'' {{WS|goodnewsabouttorrington.com}} of [[Torrington, Connecticut|Torrington]] 42 | ** ''Granby News'' {{WS|foothillsmediagroup.com/granby}} of [[Granby, Connecticut|Granby]] 43 | ** ''Canton News'' {{WS|foothillsmediagroup.com/canton}} of [[Canton, Connecticut|Canton]] 44 | ** ''Avon News'' {{WS|foothillsmediagroup.com/avon}} of [[Avon, Connecticut|Avon]] 45 | ** ''Simsbury News'' {{WS|foothillsmediagroup.com/simsbury}} of [[Simsbury, Connecticut|Simsbury]] 46 | ** ''Litchfield News'' {{WS|foothillsmediagroup.com/litchfield}} of [[Litchfield, Connecticut|Litchfield]] 47 | ** ''Foothills Trader'' {{WS|foothillstrader.com}} of Torrington, Bristol, Canton 48 | 49 | * Other weeklies 50 | ** ''The Milford-Orange Bulletin'' {{WS|ctbulletin.com}} of [[Orange, Connecticut|Orange]] 51 | ** ''The Post-Chronicle'' {{WS|ctpostchronicle.com}} of [[North Haven, Connecticut|North Haven]] 52 | ** ''West Hartford News'' {{WS|westhartfordnews.com}} of [[West Hartford, Connecticut|West Hartford]] 53 | 54 | * Magazines 55 | ** ''The Connecticut Bride'' {{WS|connecticutmag.com}} 56 | ** ''Connecticut Magazine'' {{WS|theconnecticutbride.com}} 57 | ** ''Passport Magazine'' {{WS|passport-mag.com}} 58 | 59 | == Michigan == 60 | Four dailies, associated weeklies and [[pennysaver]]s in the state of [[Michigan]]; also [http://www.micentralhomes.com MIcentralhomes.com] and [http://www.micentralautos.com MIcentralautos.com] 61 | * ''[[Oakland Press]]'' {{WS|theoaklandpress.com}} of [[Oakland, Michigan|Oakland]] 62 | * ''Daily Tribune'' {{WS|dailytribune.com}} of [[Royal Oak, Michigan|Royal Oak]] 63 | * ''Macomb Daily'' {{WS|macombdaily.com}} of [[Mt. Clemens, Michigan|Mt. Clemens]] 64 | * ''[[Morning Sun]]'' {{WS|themorningsun.com}} of [[Mount Pleasant, Michigan|Mount Pleasant]] 65 | 66 | * Heritage Newspapers {{WS|heritage.com}} 67 | ** ''Belleville View'' {{WS|bellevilleview.com}} 68 | ** ''Ile Camera'' {{WS|thenewsherald.com/ile_camera}} 69 | ** ''Monroe Guardian'' {{WS|monreguardian.com}} 70 | ** ''Ypsilanti Courier'' {{WS|ypsilanticourier.com}} 71 | ** ''News-Herald'' {{WS|thenewsherald.com}} 72 | ** ''Press & Guide'' {{WS|pressandguide.com}} 73 | ** ''Chelsea Standard & Dexter Leader'' {{WS|chelseastandard.com}} 74 | ** ''Manchester Enterprise'' {{WS|manchesterguardian.com}} 75 | ** ''Milan News-Leader'' {{WS|milannews.com}} 76 | ** ''Saline Reporter'' {{WS|salinereporter.com}} 77 | * Independent Newspapers 78 | ** ''Advisor'' {{WS|sourcenewspapers.com}} 79 | ** ''Source'' {{WS|sourcenewspapers.com}} 80 | * Morning Star {{WS|morningstarpublishing.com}} 81 | ** ''The Leader & Kalkaskian'' {{WS|leaderandkalkaskian.com}} 82 | ** ''Grand Traverse Insider'' {{WS|grandtraverseinsider.com}} 83 | ** ''Alma Reminder'' 84 | ** ''Alpena Star'' 85 | ** ''Ogemaw/Oscoda County Star'' 86 | ** ''Presque Isle Star'' 87 | ** ''St. Johns Reminder'' 88 | 89 | * Voice Newspapers {{WS|voicenews.com}} 90 | ** ''Armada Times'' 91 | ** ''Bay Voice'' 92 | ** ''Blue Water Voice'' 93 | ** ''Downriver Voice'' 94 | ** ''Macomb Township Voice'' 95 | ** ''North Macomb Voice'' 96 | ** ''Weekend Voice'' 97 | 98 | == Mid-Hudson == 99 | One daily, associated magazines in the [[Hudson River Valley]] of [[New York]]; also [http://www.midhudsoncentral.com MidHudsonCentral.com] and [http://www.jobsinnewyork.com JobsInNewYork.com]. 100 | 101 | * ''[[Daily Freeman]]'' {{WS|dailyfreeman.com}} of [[Kingston, New York]] 102 | * ''Las Noticias'' {{WS|lasnoticiasny.com}} of [[Kingston, New York]] 103 | 104 | == Ohio == 105 | Two dailies, associated magazines and three shared Websites, all in the state of [[Ohio]]: [http://www.allaroundcleveland.com AllAroundCleveland.com], [http://www.allaroundclevelandcars.com AllAroundClevelandCars.com] and [http://www.allaroundclevelandjobs.com AllAroundClevelandJobs.com]. 106 | 107 | * ''[[The News-Herald (Ohio)|The News-Herald]]'' {{WS|news-herald.com}} of [[Willoughby, Ohio|Willoughby]] 108 | * ''[[The Morning Journal]]'' {{WS|morningjournal.com}} of [[Lorain, Ohio|Lorain]] 109 | * ''El Latino Expreso'' {{WS|lorainlatino.com}} of [[Lorain, Ohio|Lorain]] 110 | 111 | == Philadelphia area == 112 | Seven dailies and associated weeklies and magazines in [[Pennsylvania]] and [[New Jersey]], and associated Websites: [http://www.allaroundphilly.com AllAroundPhilly.com], [http://www.jobsinnj.com JobsInNJ.com], [http://www.jobsinpa.com JobsInPA.com], and [http://www.phillycarsearch.com PhillyCarSearch.com]. 113 | 114 | * ''[[The Daily Local News]]'' {{WS|dailylocal.com}} of [[West Chester, Pennsylvania|West Chester]] 115 | * ''[[Delaware County Daily and Sunday Times]] {{WS|delcotimes.com}} of Primos [[Upper Darby Township, Pennsylvania]] 116 | * ''[[The Mercury (Pennsylvania)|The Mercury]]'' {{WS|pottstownmercury.com}} of [[Pottstown, Pennsylvania|Pottstown]] 117 | * ''[[The Reporter (Lansdale)|The Reporter]]'' {{WS|thereporteronline.com}} of [[Lansdale, Pennsylvania|Lansdale]] 118 | * ''The Times Herald'' {{WS|timesherald.com}} of [[Norristown, Pennsylvania|Norristown]] 119 | * ''[[The Trentonian]]'' {{WS|trentonian.com}} of [[Trenton, New Jersey]] 120 | 121 | * Weeklies 122 | * ''The Phoenix'' {{WS|phoenixvillenews.com}} of [[Phoenixville, Pennsylvania]] 123 | ** ''El Latino Expreso'' {{WS|njexpreso.com}} of [[Trenton, New Jersey]] 124 | ** ''La Voz'' {{WS|lavozpa.com}} of [[Norristown, Pennsylvania]] 125 | ** ''The Tri County Record'' {{WS|tricountyrecord.com}} of [[Morgantown, Pennsylvania]] 126 | ** ''Penny Pincher'' {{WS|pennypincherpa.com}}of [[Pottstown, Pennsylvania]] 127 | 128 | * Chesapeake Publishing {{WS|southernchestercountyweeklies.com}} 129 | ** ''The Kennett Paper'' {{WS|kennettpaper.com}} of [[Kennett Square, Pennsylvania]] 130 | ** ''Avon Grove Sun'' {{WS|avongrovesun.com}} of [[West Grove, Pennsylvania]] 131 | ** ''The Central Record'' {{WS|medfordcentralrecord.com}} of [[Medford, New Jersey]] 132 | ** ''Maple Shade Progress'' {{WS|mapleshadeprogress.com}} of [[Maple Shade, New Jersey]] 133 | 134 | * Intercounty Newspapers {{WS|buckslocalnews.com}} {{WS|southjerseylocalnews.com}} 135 | ** ''The Pennington Post'' {{WS|penningtonpost.com}} of [[Pennington, New Jersey]] 136 | ** ''The Bristol Pilot'' {{WS|bristolpilot.com}} of [[Bristol, Pennsylvania]] 137 | ** ''Yardley News'' {{WS|yardleynews.com}} of [[Yardley, Pennsylvania]] 138 | ** ''Advance of Bucks County'' {{WS|advanceofbucks.com}} of [[Newtown, Pennsylvania]] 139 | ** ''Record Breeze'' {{WS|recordbreeze.com}} of [[Berlin, New Jersey]] 140 | ** ''Community News'' {{WS|sjcommunitynews.com}} of [[Pemberton, New Jersey]] 141 | 142 | * Montgomery Newspapers {{WS|montgomerynews.com}} 143 | ** ''Ambler Gazette'' {{WS|amblergazette.com}} of [[Ambler, Pennsylvania]] 144 | ** ''The Colonial'' {{WS|colonialnews.com}} of [[Plymouth Meeting, Pennsylvania]] 145 | ** ''Glenside News'' {{WS|glensidenews.com}} of [[Glenside, Pennsylvania]] 146 | ** ''The Globe'' {{WS|globenewspaper.com}} of [[Lower Moreland Township, Pennsylvania]] 147 | ** ''Montgomery Life'' {{WS|montgomerylife.com}} of [[Fort Washington, Pennsylvania]] 148 | ** ''North Penn Life'' {{WS|northpennlife.com}} of [[Lansdale, Pennsylvania]] 149 | ** ''Perkasie News Herald'' {{WS|perkasienewsherald.com}} of [[Perkasie, Pennsylvania]] 150 | ** ''Public Spirit'' {{WS|thepublicspirit.com}} of [[Hatboro, Pennsylvania]] 151 | ** ''Souderton Independent'' {{WS|soudertonindependent.com}} of [[Souderton, Pennsylvania]] 152 | ** ''Springfield Sun'' {{WS|springfieldsun.com}} of [[Springfield, Pennsylvania]] 153 | ** ''Spring-Ford Reporter'' {{WS|springfordreporter.com}} of [[Royersford, Pennsylvania]] 154 | ** ''Times Chronicle'' {{WS|thetimeschronicle.com}} of [[Jenkintown, Pennsylvania]] 155 | ** ''Valley Item'' {{WS|valleyitem.com}} of [[Perkiomenville, Pennsylvania]] 156 | ** ''Willow Grove Guide'' {{WS|willowgroveguide.com}} of [[Willow Grove, Pennsylvania]] 157 | ** ''The Review'' {{WS|roxreview.com}} of [[Roxborough, Philadelphia, Pennsylvania]] 158 | 159 | * Main Line Media News {{WS|mainlinemedianews.com}} 160 | ** ''Main Line Times'' {{WS|mainlinetimes.com}} of [[Ardmore, Pennsylvania]] 161 | ** ''Main Line Life'' {{WS|mainlinelife.com}} of [[Ardmore, Pennsylvania]] 162 | ** ''The King of Prussia Courier'' {{WS|kingofprussiacourier.com}} of [[King of Prussia, Pennsylvania]] 163 | 164 | * Delaware County News Network {{WS|delconewsnetwork.com}} 165 | ** ''News of Delaware County'' {{WS|newsofdelawarecounty.com}} of [[Havertown, Pennsylvania]] 166 | ** ''County Press'' {{WS|countypressonline.com}} of [[Newtown Square, Pennsylvania]] 167 | ** ''Garnet Valley Press'' {{WS|countypressonline.com}} of [[Glen Mills, Pennsylvania]] 168 | ** ''Springfield Press'' {{WS|countypressonline.com}} of [[Springfield, Pennsylvania]] 169 | ** ''Town Talk'' {{WS|towntalknews.com}} of [[Ridley, Pennsylvania]] 170 | 171 | * Berks-Mont Newspapers {{WS|berksmontnews.com}} 172 | ** ''The Boyertown Area Times'' {{WS|berksmontnews.com/boyertown_area_times}} of [[Boyertown, Pennsylvania]] 173 | ** ''The Kutztown Area Patriot'' {{WS|berksmontnews.com/kutztown_area_patriot}} of [[Kutztown, Pennsylvania]] 174 | ** ''The Hamburg Area Item'' {{WS|berksmontnews.com/hamburg_area_item}} of [[Hamburg, Pennsylvania]] 175 | ** ''The Southern Berks News'' {{WS|berksmontnews.com/southern_berks_news}} of [[Exeter Township, Berks County, Pennsylvania]] 176 | ** ''Community Connection'' {{WS|berksmontnews.com/community_connection}} of [[Boyertown, Pennsylvania]] 177 | 178 | * Magazines 179 | ** ''Bucks Co. Town & Country Living'' {{WS|buckscountymagazine.com}} 180 | ** ''Parents Express'' {{WS|parents-express.com}} 181 | ** ''Real Men, Rednecks'' {{WS|realmenredneck.com}} 182 | 183 | {{JRC}} 184 | 185 | ==References== 186 | 187 | 188 | [[Category:Journal Register publications|*]] 189 | -------------------------------------------------------------------------------- /DiffMatchPatch.PerformanceTest/left.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhgbrt/google-diff-match-patch-csharp/4ec728b96f3c793b6b824545c125e2f19e14ce08/DiffMatchPatch.PerformanceTest/left.txt -------------------------------------------------------------------------------- /DiffMatchPatch.PerformanceTest/right.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhgbrt/google-diff-match-patch-csharp/4ec728b96f3c793b6b824545c125e2f19e14ce08/DiffMatchPatch.PerformanceTest/right.txt -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/BitapAlgorithmTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Xunit; 3 | 4 | namespace DiffMatchPatch.Tests 5 | { 6 | 7 | public class BitapAlgorithmTests 8 | { 9 | [Fact] 10 | public void InitAlphabet_UniqueSet_ReturnsExpectedBitmask() 11 | { 12 | var bitmask = new Dictionary 13 | { 14 | {'a', 4}, 15 | {'b', 2}, 16 | {'c', 1} 17 | }; 18 | Assert.Equal(bitmask, BitapAlgorithm.InitAlphabet("abc")); 19 | 20 | } 21 | 22 | [Fact] 23 | public void InitAlphabet_SetWithDuplicates_ReturnsExpectedBitmask() 24 | { 25 | 26 | var bitmask = new Dictionary 27 | { 28 | {'a', 37}, 29 | {'b', 18}, 30 | {'c', 8} 31 | }; 32 | Assert.Equal(bitmask, BitapAlgorithm.InitAlphabet("abcaba")); 33 | } 34 | 35 | [Fact] 36 | public void Match_Exact1() 37 | { 38 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 39 | Assert.Equal(5, dmp.Match("abcdefghijk", "fgh", 5)); 40 | } 41 | [Fact] 42 | public void Match_Exact2() 43 | { 44 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 45 | Assert.Equal(5, dmp.Match("abcdefghijk", "fgh", 0)); 46 | } 47 | 48 | 49 | [Fact] 50 | public void Match_Fuzzy1() 51 | { 52 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 53 | Assert.Equal(4, dmp.Match("abcdefghijk", "efxhi", 0)); 54 | } 55 | [Fact] 56 | public void Match_Fuzzy2() 57 | { 58 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 59 | Assert.Equal(2, dmp.Match("abcdefghijk", "cdefxyhijk", 5)); 60 | } 61 | [Fact] 62 | public void Match_Fuzzy3() 63 | { 64 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 65 | Assert.Equal(-1, dmp.Match("abcdefghijk", "bxy", 1)); 66 | } 67 | 68 | [Fact] 69 | public void Match_Overflow() 70 | { 71 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 72 | Assert.Equal(2, dmp.Match("123456789xx0", "3456789x0", 2)); 73 | } 74 | 75 | 76 | [Fact] 77 | public void Match_BeforeStartMatch() 78 | { 79 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 80 | Assert.Equal(0, dmp.Match("abcdef", "xxabc", 4)); 81 | } 82 | 83 | [Fact] 84 | public void Match_BeyondEndMatch() 85 | { 86 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 87 | Assert.Equal(3, dmp.Match("abcdef", "defyy", 4)); 88 | } 89 | 90 | [Fact] 91 | public void Match_OversizedPattern() 92 | { 93 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 94 | Assert.Equal(0, dmp.Match("abcdef", "xabcdefy", 0)); 95 | } 96 | 97 | [Fact] 98 | public void Match_Treshold1() 99 | { 100 | var dmp = new BitapAlgorithm(new MatchSettings(0.4f, 100)); 101 | Assert.Equal(4, dmp.Match("abcdefghijk", "efxyhi", 1)); 102 | 103 | } 104 | [Fact] 105 | public void Match_Treshold2() 106 | { 107 | var dmp = new BitapAlgorithm(new MatchSettings(0.3f, 100)); 108 | Assert.Equal(-1, dmp.Match("abcdefghijk", "efxyhi", 1)); 109 | 110 | } 111 | [Fact] 112 | public void Match_Treshold3() 113 | { 114 | var dmp = new BitapAlgorithm(new MatchSettings(0.0f, 100)); 115 | Assert.Equal(1, dmp.Match("abcdefghijk", "bcdef", 1)); 116 | 117 | } 118 | 119 | [Fact] 120 | public void Match_MultipleSelect1() 121 | { 122 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 123 | Assert.Equal(0, dmp.Match("abcdexyzabcde", "abccde", 3)); 124 | } 125 | [Fact] 126 | public void Match_MultipleSelect2() 127 | { 128 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 100)); 129 | Assert.Equal(8, dmp.Match("abcdexyzabcde", "abccde", 5)); 130 | } 131 | 132 | [Fact] 133 | public void Match_DistanceTest1() 134 | { 135 | 136 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 10)); 137 | Assert.Equal(-1, dmp.Match("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); 138 | } 139 | 140 | [Fact] 141 | public void Match_DistanceTest2() 142 | { 143 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 10)); 144 | Assert.Equal(0, dmp.Match("abcdefghijklmnopqrstuvwxyz", "abcdxxefg", 1)); 145 | } 146 | [Fact] 147 | public void Match_DistanceTest3() 148 | { 149 | var dmp = new BitapAlgorithm(new MatchSettings(0.5f, 1000)); 150 | Assert.Equal(0, dmp.Match("abcdefghijklmnopqrstuvwxyz", "abcdefg", 24)); 151 | } 152 | 153 | } 154 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffListTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | 4 | using Xunit; 5 | 6 | namespace DiffMatchPatch.Tests 7 | { 8 | 9 | public class DiffListTests 10 | { 11 | [Fact] 12 | public void DiffPrettyHtmlTest() 13 | { 14 | 15 | // Pretty print. 16 | var diffs = new List 17 | { 18 | Diff.Equal("a\n"), 19 | Diff.Delete("b"), 20 | Diff.Insert("c&d") 21 | }; 22 | Assert.Equal( 23 | "
<B>b</B>c&d", 24 | diffs.PrettyHtml()); 25 | } 26 | 27 | [Fact] 28 | public void Text1_ReturnsText1() 29 | { 30 | var diffs = new List 31 | { 32 | Diff.Equal("jump"), 33 | Diff.Delete("s"), 34 | Diff.Insert("ed"), 35 | Diff.Equal(" over "), 36 | Diff.Delete("the"), 37 | Diff.Insert("a"), 38 | Diff.Equal(" lazy") 39 | }; 40 | Assert.Equal("jumps over the lazy", diffs.Text1()); 41 | } 42 | 43 | [Fact] 44 | public void Text2_ReturnsText2() 45 | { 46 | // Compute the source and destination texts. 47 | var diffs = new List 48 | { 49 | Diff.Equal("jump"), 50 | Diff.Delete("s"), 51 | Diff.Insert("ed"), 52 | Diff.Equal(" over "), 53 | Diff.Delete("the"), 54 | Diff.Insert("a"), 55 | Diff.Equal(" lazy") 56 | }; 57 | Assert.Equal("jumped over a lazy", diffs.Text2()); 58 | } 59 | 60 | [Fact] 61 | public void FindEquivalentLocation2_LocationInEquality_FindsLocation() 62 | { 63 | 64 | // Translate a location in text1 to text2. 65 | var diffs = new List 66 | { 67 | Diff.Delete("a"), 68 | Diff.Insert("1234"), 69 | Diff.Equal("xyz") 70 | }; 71 | Assert.Equal(5, diffs.FindEquivalentLocation2(2)); 72 | } 73 | [Fact] 74 | public void FindEquivalentLocation2_LocationOnDeletion_FindsLocation() 75 | { 76 | 77 | var diffs = new List 78 | { 79 | Diff.Equal("a"), 80 | Diff.Delete("1234"), 81 | Diff.Equal("xyz") 82 | }; 83 | Assert.Equal(1, diffs.FindEquivalentLocation2(3)); 84 | } 85 | 86 | [Fact] 87 | public void Levenshtein_WithTrailingEquality() 88 | { 89 | 90 | var diffs = new List 91 | { 92 | Diff.Delete("abc"), 93 | Diff.Insert("1234"), 94 | Diff.Equal("xyz") 95 | }; 96 | Assert.Equal(4, diffs.Levenshtein()); 97 | } 98 | [Fact] 99 | public void Levenshtein_WithLeadingEquality() 100 | { 101 | var diffs = new List 102 | { 103 | Diff.Equal("xyz"), 104 | Diff.Delete("abc"), 105 | Diff.Insert("1234") 106 | }; 107 | Assert.Equal(4, diffs.Levenshtein()); 108 | 109 | } 110 | [Fact] 111 | public void Levenshtein_WithMiddleEquality() 112 | { 113 | var diffs = new List 114 | { 115 | Diff.Delete("abc"), 116 | Diff.Equal("xyz"), 117 | Diff.Insert("1234") 118 | }; 119 | Assert.Equal(7, diffs.Levenshtein()); 120 | } 121 | [Fact] 122 | public void DiffBisectTest_NoTimeout() 123 | { 124 | 125 | // Normal. 126 | var a = "cat"; 127 | var b = "map"; 128 | // Since the resulting diff hasn't been normalized, it would be ok if 129 | // the insertion and deletion pairs are swapped. 130 | // If the order changes, tweak this test as required. 131 | var diffs = new List 132 | { 133 | Diff.Delete("c"), 134 | Diff.Insert("m"), 135 | Diff.Equal("a"), 136 | Diff.Delete("t"), 137 | Diff.Insert("p") 138 | }; 139 | Assert.Equal(diffs, DiffAlgorithm.MyersDiffBisect(a, b, false, new CancellationToken())); 140 | } 141 | 142 | 143 | [Fact] 144 | public void DiffBisectTest_WithTimeout() 145 | { 146 | var a = "cat"; 147 | var b = "map"; 148 | 149 | var diffs = new List { Diff.Delete("cat"), Diff.Insert("map") }; 150 | Assert.Equal(diffs, DiffAlgorithm.MyersDiffBisect(a, b, true, new CancellationToken(true))); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffList_CharsToLinesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace DiffMatchPatch.Tests 8 | { 9 | 10 | public class DiffList_CharsToLinesTests 11 | { 12 | [Fact] 13 | public void CharsToLines_ValidCharsWithCorrespondingLines_RestoresDiffsCorrectly() 14 | { 15 | // Convert chars up to lines. 16 | var diffs = new List 17 | { 18 | Diff.Equal("\u0001\u0002\u0001"), 19 | Diff.Insert("\u0002\u0001\u0002") 20 | }; 21 | var tmpVector = new List {"", "alpha\n", "beta\n"}; 22 | var expected = new List 23 | { 24 | Diff.Equal("alpha\nbeta\nalpha\n"), 25 | Diff.Insert("beta\nalpha\nbeta\n") 26 | }; 27 | var result = diffs.CharsToLines(tmpVector).ToList(); 28 | Assert.Equal(expected, result); 29 | } 30 | 31 | [Fact] 32 | public void CharsToLines_MoreThan256Chars_RestoresDiffCorrectly() 33 | { 34 | 35 | // More than 256 to reveal any 8-bit limitations. 36 | var n = 300; 37 | var tmpVector = new List(); 38 | var lineList = new StringBuilder(); 39 | var charList = new StringBuilder(); 40 | for (var x = 1; x < n + 1; x++) 41 | { 42 | tmpVector.Add(x + "\n"); 43 | lineList.Append(x + "\n"); 44 | charList.Append(Convert.ToChar(x)); 45 | } 46 | Assert.Equal(n, tmpVector.Count); 47 | var lines = lineList.ToString(); 48 | var chars = charList.ToString(); 49 | Assert.Equal(n, chars.Length); 50 | tmpVector.Insert(0, ""); 51 | var diffs = new [] 52 | { 53 | Diff.Delete(chars) 54 | }; 55 | 56 | var result = diffs.CharsToLines(tmpVector).ToList(); 57 | 58 | var expected = new List 59 | { 60 | Diff.Delete(lines) 61 | }; 62 | Assert.Equal(expected, result); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffList_CleanupEfficiencyTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Xunit; 3 | 4 | namespace DiffMatchPatch.Tests 5 | { 6 | 7 | public class DiffList_CleanupEfficiencyTests 8 | { 9 | [Fact] 10 | public void EmptyList() 11 | { 12 | var diffs = new List(); 13 | diffs.CleanupEfficiency(); 14 | Assert.Equal(new List(), diffs); 15 | } 16 | 17 | [Fact] 18 | public void NoElimination() 19 | { 20 | var diffs = new List 21 | { 22 | Diff.Delete("ab"), 23 | Diff.Insert("12"), 24 | Diff.Equal("wxyz"), 25 | Diff.Delete("cd"), 26 | Diff.Insert("34") 27 | }; 28 | diffs.CleanupEfficiency(); 29 | Assert.Equal(new List 30 | { 31 | Diff.Delete("ab"), 32 | Diff.Insert("12"), 33 | Diff.Equal("wxyz"), 34 | Diff.Delete("cd"), 35 | Diff.Insert("34") 36 | }, diffs); 37 | } 38 | 39 | [Fact] 40 | public void FourEditElimination() 41 | { 42 | var diffs = new List 43 | { 44 | Diff.Delete("ab"), 45 | Diff.Insert("12"), 46 | Diff.Equal("xyz"), 47 | Diff.Delete("cd"), 48 | Diff.Insert("34") 49 | }.CleanupEfficiency(); 50 | Assert.Equal(new List 51 | { 52 | Diff.Delete("abxyzcd"), 53 | Diff.Insert("12xyz34") 54 | }, diffs); 55 | } 56 | 57 | [Fact] 58 | public void ThreeEditElimination() 59 | { 60 | var diffs = new List 61 | { 62 | Diff.Insert("12"), 63 | Diff.Equal("x"), 64 | Diff.Delete("cd"), 65 | Diff.Insert("34") 66 | }.CleanupEfficiency(); 67 | Assert.Equal(new List 68 | { 69 | Diff.Delete("xcd"), 70 | Diff.Insert("12x34") 71 | }, diffs); 72 | } 73 | 74 | [Fact] 75 | public void BackpassElimination() 76 | { 77 | var diffs = new List 78 | { 79 | Diff.Delete("ab"), 80 | Diff.Insert("12"), 81 | Diff.Equal("xy"), 82 | Diff.Insert("34"), 83 | Diff.Equal("z"), 84 | Diff.Delete("cd"), 85 | Diff.Insert("56") 86 | }.CleanupEfficiency(); 87 | Assert.Equal(new List 88 | { 89 | Diff.Delete("abxyzcd"), 90 | Diff.Insert("12xy34z56") 91 | }, diffs); 92 | } 93 | 94 | [Fact] 95 | public void HighCostElimination() 96 | { 97 | short highDiffEditCost = 5; 98 | 99 | var diffs = new List 100 | { 101 | Diff.Delete("ab"), 102 | Diff.Insert("12"), 103 | Diff.Equal("wxyz"), 104 | Diff.Delete("cd"), 105 | Diff.Insert("34") 106 | }.CleanupEfficiency(highDiffEditCost); 107 | Assert.Equal(new List 108 | { 109 | Diff.Delete("abwxyzcd"), 110 | Diff.Insert("12wxyz34") 111 | }, diffs); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffList_CleanupMergeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | using Xunit; 5 | 6 | namespace DiffMatchPatch.Tests 7 | { 8 | 9 | public class DiffList_CleanupMergeTests 10 | { 11 | [Fact] 12 | public void CleanupMerge_EmptyDiffList_ReturnsEmptyDiffList() 13 | { 14 | // Cleanup a messy diff. 15 | // Null case. 16 | var result = new List().CleanupMerge().ToList(); 17 | var expected = new List(); 18 | 19 | Assert.Equal(expected, result); 20 | } 21 | 22 | [Fact] 23 | public void CleanupMerge_AlreadyCleaned_ReturnsSameList() 24 | { 25 | var result = new List { Diff.Equal("a"), Diff.Delete("b"), Diff.Insert("c") }.CleanupMerge().ToList(); 26 | 27 | var expected = new[] {Diff.Equal("a"), Diff.Delete("b"), Diff.Insert("c")}; 28 | 29 | Assert.Equal(expected, result); 30 | } 31 | 32 | [Fact] 33 | public void SubsequentEqualitiesAreMerged() 34 | { 35 | var diffs = new List { Diff.Equal("a"), Diff.Equal("b"), Diff.Equal("c") }.CleanupMerge().ToList(); 36 | Assert.Equal(new List { Diff.Equal("abc") }, diffs); 37 | } 38 | 39 | [Fact] 40 | public void SubsequentDeletesAreMerged() 41 | { 42 | var diffs = new List { Diff.Delete("a"), Diff.Delete("b"), Diff.Delete("c") }.CleanupMerge().ToList(); 43 | Assert.Equal(new List { Diff.Delete("abc") }, diffs); 44 | } 45 | 46 | [Fact] 47 | public void SubsequentInsertsAreMerged() 48 | { 49 | var diffs = new List { Diff.Insert("a"), Diff.Insert("b"), Diff.Insert("c") }.CleanupMerge().ToList(); 50 | Assert.Equal(new List { Diff.Insert("abc") }, diffs); 51 | } 52 | 53 | [Fact] 54 | public void InterweavedInsertDeletesAreMerged() 55 | { 56 | 57 | // Merge interweave. 58 | var diffs = new List 59 | { 60 | Diff.Delete("a"), 61 | Diff.Insert("b"), 62 | Diff.Delete("c"), 63 | Diff.Insert("d"), 64 | Diff.Equal("e"), 65 | Diff.Equal("f") 66 | }.CleanupMerge().ToList(); 67 | Assert.Equal(new List { Diff.Delete("ac"), Diff.Insert("bd"), Diff.Equal("ef") }, diffs); 68 | } 69 | 70 | 71 | [Fact] 72 | public void PrefixSuffixDetection() 73 | { 74 | 75 | // Prefix and suffix detection. 76 | var diffs = new List { Diff.Delete("a"), Diff.Insert("abc"), Diff.Delete("dc") }.CleanupMerge().ToList(); 77 | Assert.Equal( 78 | new List { Diff.Equal("a"), Diff.Delete("d"), Diff.Insert("b"), Diff.Equal("c") }, diffs); 79 | } 80 | [Fact] 81 | public void PrefixSuffixDetectionWithEqualities() 82 | { 83 | 84 | // Prefix and suffix detection. 85 | var diffs = new List 86 | { 87 | Diff.Equal("x"), 88 | Diff.Delete("a"), 89 | Diff.Insert("abc"), 90 | Diff.Delete("dc"), 91 | Diff.Equal("y") 92 | }.CleanupMerge().ToList(); 93 | Assert.Equal( 94 | new List { Diff.Equal("xa"), Diff.Delete("d"), Diff.Insert("b"), Diff.Equal("cy") }, diffs); 95 | } 96 | 97 | 98 | [Fact] 99 | public void SlideEditLeft() 100 | { 101 | // Slide edit left. 102 | var diffs = new List { Diff.Equal("a"), Diff.Insert("ba"), Diff.Equal("c") }.CleanupMerge().ToList(); 103 | Assert.Equal(new List { Diff.Insert("ab"), Diff.Equal("ac") }, diffs); 104 | } 105 | 106 | 107 | [Fact] 108 | public void SlideEditRight() 109 | { 110 | 111 | // Slide edit right. 112 | var diffs = new List { Diff.Equal("c"), Diff.Insert("ab"), Diff.Equal("a") }.CleanupMerge().ToList(); 113 | Assert.Equal(new List { Diff.Equal("ca"), Diff.Insert("ba") }, diffs); 114 | 115 | } 116 | 117 | 118 | [Fact] 119 | public void SlideEditLeftRecursive() 120 | { 121 | // Slide edit left recursive. 122 | var diffs = new List 123 | { 124 | Diff.Equal("a"), 125 | Diff.Delete("b"), 126 | Diff.Equal("c"), 127 | Diff.Delete("ac"), 128 | Diff.Equal("x") 129 | }.CleanupMerge().ToList(); 130 | Assert.Equal(new List { Diff.Delete("abc"), Diff.Equal("acx") }, diffs); 131 | 132 | } 133 | 134 | 135 | [Fact] 136 | public void SlideEditRightRecursive() 137 | { 138 | // Slide edit right recursive. 139 | var diffs = new List 140 | { 141 | Diff.Equal("x"), 142 | Diff.Delete("ca"), 143 | Diff.Equal("c"), 144 | Diff.Delete("b"), 145 | Diff.Equal("a") 146 | }.CleanupMerge().ToList(); 147 | Assert.Equal(new List { Diff.Equal("xca"), Diff.Delete("cba") }, diffs); 148 | } 149 | 150 | [Fact] 151 | public void EmptyMerge() 152 | { 153 | var diffs = new List 154 | { 155 | Diff.Delete("b"), 156 | Diff.Insert("ab"), 157 | Diff.Equal("c") 158 | }.CleanupMerge().ToList(); 159 | Assert.Equal(new List { Diff.Insert("a"), Diff.Equal("bc") }, diffs); 160 | } 161 | 162 | [Fact] 163 | public void EmptyEquality() 164 | { 165 | var diffs = new List 166 | { 167 | Diff.Equal(""), 168 | Diff.Insert("a"), 169 | Diff.Equal("b") 170 | }.CleanupMerge().ToList(); 171 | Assert.Equal(new List { Diff.Insert("a"), Diff.Equal("b") }, diffs); 172 | 173 | } 174 | 175 | [Fact] 176 | public void FourEditElimination() 177 | { 178 | var diffs = new List 179 | { 180 | Diff.Delete("ab"), 181 | Diff.Insert("12"), 182 | Diff.Equal("xyz"), 183 | Diff.Delete("cd"), 184 | Diff.Insert("34") 185 | }.CleanupMerge().ToList(); 186 | Assert.Equal(new List 187 | { 188 | Diff.Delete("ab"), 189 | Diff.Insert("12"), 190 | Diff.Equal("xyz"), 191 | Diff.Delete("cd"), 192 | Diff.Insert("34") 193 | }, diffs); 194 | } 195 | 196 | } 197 | 198 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffList_CleanupSemanticLosslessTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace DiffMatchPatch.Tests 6 | { 7 | 8 | public class DiffList_CleanupSemanticLosslessTests 9 | { 10 | [Fact] 11 | public void EmptyList_WhenCleaned_RemainsEmptyList() 12 | { 13 | // Slide Diffs to match logical boundaries. 14 | // Null case. 15 | var diffs = new List().CleanupSemanticLossless().ToList(); 16 | Assert.Equal(new List(), diffs); 17 | } 18 | 19 | [Fact] 20 | public void SingleDiff_WhenCleaned_Remains() 21 | { 22 | // Blank lines. 23 | var diffs = new List 24 | { 25 | Diff.Equal("AAA"), 26 | }.CleanupSemanticLossless().ToList(); 27 | Assert.Equal(new List 28 | { 29 | Diff.Equal("AAA"), 30 | }, diffs); 31 | } 32 | 33 | [Fact] 34 | public void TwoDiffs_WhenCleaned_Remains() 35 | { 36 | // Blank lines. 37 | var diffs = new List 38 | { 39 | Diff.Equal("AAA"), 40 | Diff.Insert("BBB"), 41 | }.CleanupSemanticLossless().ToList(); 42 | Assert.Equal(new List 43 | { 44 | Diff.Equal("AAA"), 45 | Diff.Insert("BBB"), 46 | }, diffs); 47 | } 48 | [Fact] 49 | public void ThreeDiffs_WhenCleaned_Remains() 50 | { 51 | // Blank lines. 52 | var diffs = new List 53 | { 54 | Diff.Equal("AAA"), 55 | Diff.Insert("BBB"), 56 | Diff.Delete("CCC"), 57 | }.CleanupSemanticLossless().ToList(); 58 | Assert.Equal(new List 59 | { 60 | Diff.Equal("AAA"), 61 | Diff.Insert("BBB"), 62 | Diff.Delete("CCC"), 63 | }, diffs); 64 | } 65 | [Fact] 66 | public void FourDiffs_WhenCleaned_Remains() 67 | { 68 | // Blank lines. 69 | var diffs = new List 70 | { 71 | Diff.Equal("AAA"), 72 | Diff.Insert("BBB"), 73 | Diff.Delete("CCC"), 74 | Diff.Equal("DDD"), 75 | }.CleanupSemanticLossless().ToList(); 76 | Assert.Equal(new List 77 | { 78 | Diff.Equal("AAA"), 79 | Diff.Insert("BBB"), 80 | Diff.Delete("CCC"), 81 | Diff.Equal("DDD"), 82 | }, diffs); 83 | } 84 | 85 | 86 | [Fact] 87 | public void BlankLines() 88 | { 89 | // Blank lines. 90 | var diffs = new List 91 | { 92 | Diff.Equal("AAA\r\n\r\nBBB"), 93 | Diff.Insert("\r\nDDD\r\n\r\nBBB"), 94 | Diff.Equal("\r\nEEE") 95 | }.CleanupSemanticLossless().ToList(); 96 | Assert.Equal(new List 97 | { 98 | Diff.Equal("AAA\r\n\r\n"), 99 | Diff.Insert("BBB\r\nDDD\r\n\r\n"), 100 | Diff.Equal("BBB\r\nEEE") 101 | }, diffs); 102 | } 103 | 104 | [Fact] 105 | public void NoCleanup() 106 | { 107 | // Line boundaries. 108 | var diffs = new List 109 | { 110 | Diff.Equal("AAA\r\n"), 111 | Diff.Insert("BBB DDD\r\n"), 112 | Diff.Equal("BBB EEE\r\n"), 113 | Diff.Insert("FFF GGG\r\n"), 114 | Diff.Equal("HHH III"), 115 | }.CleanupSemanticLossless().ToList(); 116 | Assert.Equal(new List 117 | { 118 | Diff.Equal("AAA\r\n"), 119 | Diff.Insert("BBB DDD\r\n"), 120 | Diff.Equal("BBB EEE\r\n"), 121 | Diff.Insert("FFF GGG\r\n"), 122 | Diff.Equal("HHH III"), 123 | }, diffs); 124 | 125 | } 126 | 127 | [Fact] 128 | public void LineBoundaries() 129 | { 130 | 131 | // Line boundaries. 132 | var diffs = new List 133 | { 134 | Diff.Equal("AAA\r\nBBB"), 135 | Diff.Insert(" DDD\r\nBBB"), 136 | Diff.Equal(" EEE") 137 | }.CleanupSemanticLossless().ToList(); 138 | Assert.Equal(new List 139 | { 140 | Diff.Equal("AAA\r\n"), 141 | Diff.Insert("BBB DDD\r\n"), 142 | Diff.Equal("BBB EEE") 143 | }, diffs); 144 | } 145 | 146 | [Fact] 147 | public void WordBoundaries() 148 | { 149 | var diffs = new List 150 | { 151 | Diff.Equal("The c"), 152 | Diff.Insert("ow and the c"), 153 | Diff.Equal("at.") 154 | }.CleanupSemanticLossless().ToList(); 155 | Assert.Equal(new List 156 | { 157 | Diff.Equal("The "), 158 | Diff.Insert("cow and the "), 159 | Diff.Equal("cat.") 160 | }, diffs); 161 | } 162 | 163 | [Fact] 164 | public void AlphaNumericBoundaries() 165 | { 166 | // Alphanumeric boundaries. 167 | var diffs = new List 168 | { 169 | Diff.Equal("The-c"), 170 | Diff.Insert("ow-and-the-c"), 171 | Diff.Equal("at.") 172 | }.CleanupSemanticLossless().ToList(); 173 | Assert.Equal(new List 174 | { 175 | Diff.Equal("The-"), 176 | Diff.Insert("cow-and-the-"), 177 | Diff.Equal("cat.") 178 | }, diffs); 179 | } 180 | 181 | [Fact] 182 | public void HittingTheStart() 183 | { 184 | var diffs = new List 185 | { 186 | Diff.Equal("a"), 187 | Diff.Delete("a"), 188 | Diff.Equal("ax") 189 | }.CleanupSemanticLossless().ToList(); 190 | Assert.Equal(new List 191 | { 192 | Diff.Delete("a"), 193 | Diff.Equal("aax") 194 | }, diffs); 195 | } 196 | 197 | [Fact] 198 | public void HittingTheEnd() 199 | { 200 | var diffs = new List 201 | { 202 | Diff.Equal("xa"), 203 | Diff.Delete("a"), 204 | Diff.Equal("a") 205 | }.CleanupSemanticLossless().ToList(); 206 | Assert.Equal(new List 207 | { 208 | Diff.Equal("xaa"), 209 | Diff.Delete("a") 210 | }, diffs); 211 | } 212 | 213 | [Fact] 214 | public void SentenceBoundaries() 215 | { 216 | var diffs = new List 217 | { 218 | Diff.Equal("The xxx. The "), 219 | Diff.Insert("zzz. The "), 220 | Diff.Equal("yyy.") 221 | }.CleanupSemanticLossless().ToList(); 222 | Assert.Equal(new List 223 | { 224 | Diff.Equal("The xxx."), 225 | Diff.Insert(" The zzz."), 226 | Diff.Equal(" The yyy.") 227 | }, diffs); 228 | } 229 | 230 | 231 | } 232 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffList_CleanupSemanticTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Xunit; 3 | 4 | namespace DiffMatchPatch.Tests 5 | { 6 | 7 | public class DiffList_CleanupSemanticTests 8 | { 9 | [Fact] 10 | public void EmptyList() 11 | { 12 | var diffs = new List().CleanupSemantic(); 13 | Assert.Equal(new List(), diffs); 14 | } 15 | 16 | [Fact] 17 | public void NoEliminiation1() 18 | { 19 | var diffs = new List 20 | { 21 | Diff.Delete("ab"), 22 | Diff.Insert("cd"), 23 | Diff.Equal("12"), 24 | Diff.Delete("e") 25 | }.CleanupSemantic(); 26 | Assert.Equal(new List 27 | { 28 | Diff.Delete("ab"), 29 | Diff.Insert("cd"), 30 | Diff.Equal("12"), 31 | Diff.Delete("e") 32 | }, diffs); 33 | } 34 | 35 | [Fact] 36 | public void NoElimination2() 37 | { 38 | var diffs = new List 39 | { 40 | Diff.Delete("abc"), 41 | Diff.Insert("ABC"), 42 | Diff.Equal("1234"), 43 | Diff.Delete("wxyz") 44 | }.CleanupSemantic(); 45 | Assert.Equal(new List 46 | { 47 | Diff.Delete("abc"), 48 | Diff.Insert("ABC"), 49 | Diff.Equal("1234"), 50 | Diff.Delete("wxyz") 51 | }, diffs); 52 | } 53 | 54 | [Fact] 55 | public void SimpleElimination() 56 | { 57 | var diffs = new List 58 | { 59 | Diff.Delete("a"), 60 | Diff.Equal("b"), 61 | Diff.Delete("c") 62 | }.CleanupSemantic(); 63 | Assert.Equal(new List 64 | { 65 | Diff.Delete("abc"), 66 | Diff.Insert("b") 67 | }, diffs); 68 | } 69 | 70 | 71 | 72 | [Fact] 73 | public void BackpassElimination() 74 | { 75 | var diffs = new List 76 | { 77 | Diff.Delete("ab"), 78 | Diff.Equal("cd"), 79 | Diff.Delete("e"), 80 | Diff.Equal("f"), 81 | Diff.Insert("g") 82 | }.CleanupSemantic(); 83 | Assert.Equal(new List 84 | { 85 | Diff.Delete("abcdef"), 86 | Diff.Insert("cdfg") 87 | }, diffs); 88 | 89 | } 90 | 91 | [Fact] 92 | public void MultipleEliminations() 93 | { 94 | var diffs = new List 95 | { 96 | Diff.Insert("1"), 97 | Diff.Equal("A"), 98 | Diff.Delete("B"), 99 | Diff.Insert("2"), 100 | Diff.Equal("_"), 101 | Diff.Insert("1"), 102 | Diff.Equal("A"), 103 | Diff.Delete("B"), 104 | Diff.Insert("2") 105 | }.CleanupSemantic(); 106 | Assert.Equal(new List 107 | { 108 | Diff.Delete("AB_AB"), 109 | Diff.Insert("1A2_1A2") 110 | }, diffs); 111 | } 112 | 113 | [Fact] 114 | public void WordBoundaries() 115 | { 116 | var diffs = new List 117 | { 118 | Diff.Equal("The c"), 119 | Diff.Delete("ow and the c"), 120 | Diff.Equal("at.") 121 | }.CleanupSemantic(); 122 | Assert.Equal(new List 123 | { 124 | Diff.Equal("The "), 125 | Diff.Delete("cow and the "), 126 | Diff.Equal("cat.") 127 | }, diffs); 128 | } 129 | 130 | [Fact] 131 | public void NoOverlapElimination() 132 | { 133 | var diffs = new List 134 | { 135 | Diff.Delete("abcxx"), 136 | Diff.Insert("xxdef") 137 | }.CleanupSemantic(); 138 | Assert.Equal(new List 139 | { 140 | Diff.Delete("abcxx"), 141 | Diff.Insert("xxdef") 142 | }, diffs); 143 | } 144 | 145 | [Fact] 146 | public void OverlapElimination() 147 | { 148 | var diffs = new List 149 | { 150 | Diff.Delete("abcxxx"), 151 | Diff.Insert("xxxdef") 152 | }.CleanupSemantic(); 153 | Assert.Equal(new List 154 | { 155 | Diff.Delete("abc"), 156 | Diff.Equal("xxx"), 157 | Diff.Insert("def") 158 | }, diffs); 159 | } 160 | 161 | [Fact] 162 | public void ReverseOverlapElimination() 163 | { 164 | var diffs = new List 165 | { 166 | Diff.Delete("xxxabc"), 167 | Diff.Insert("defxxx") 168 | }.CleanupSemantic(); 169 | Assert.Equal(new List 170 | { 171 | Diff.Insert("def"), 172 | Diff.Equal("xxx"), 173 | Diff.Delete("abc") 174 | }, diffs); 175 | } 176 | 177 | [Fact] 178 | public void TwoOverlapEliminations() 179 | { 180 | var diffs = new List 181 | { 182 | Diff.Delete("abcd1212"), 183 | Diff.Insert("1212efghi"), 184 | Diff.Equal("----"), 185 | Diff.Delete("A3"), 186 | Diff.Insert("3BC") 187 | }.CleanupSemantic(); 188 | Assert.Equal(new List 189 | { 190 | Diff.Delete("abcd"), 191 | Diff.Equal("1212"), 192 | Diff.Insert("efghi"), 193 | Diff.Equal("----"), 194 | Diff.Delete("A"), 195 | Diff.Equal("3"), 196 | Diff.Insert("BC") 197 | }, diffs); 198 | } 199 | 200 | 201 | } 202 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffList_ToDeltaTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Xunit; 6 | 7 | namespace DiffMatchPatch.Tests 8 | { 9 | 10 | public class DiffList_ToDeltaTests 11 | { 12 | IReadOnlyCollection diffs = new List 13 | { 14 | Diff.Equal("jump"), 15 | Diff.Delete("s"), 16 | Diff.Insert("ed"), 17 | Diff.Equal(" over "), 18 | Diff.Delete("the"), 19 | Diff.Insert("a"), 20 | Diff.Equal(" lazy"), 21 | Diff.Insert("old dog") 22 | }; 23 | 24 | [Fact] 25 | public void Verify() 26 | { 27 | var text1 = diffs.Text1(); 28 | Assert.Equal("jumps over the lazy", text1); 29 | 30 | } 31 | 32 | [Fact] 33 | public void ToDelta_GeneratesExpectedOutput() 34 | { 35 | var delta = diffs.ToDelta(); 36 | Assert.Equal("=4\t-1\t+ed\t=6\t-3\t+a\t=5\t+old dog", delta); 37 | } 38 | [Fact] 39 | public void FromDelta_EmptyTokensAreOk() 40 | { 41 | var delta = "\t\t"; 42 | var diffs = DiffList.FromDelta("", delta); 43 | Assert.Empty(diffs); 44 | } 45 | 46 | [Fact] 47 | public void FromDelta_GeneratesExpectedDiffs() 48 | { 49 | var delta = diffs.ToDelta(); 50 | var result = DiffList.FromDelta(diffs.Text1(), delta); 51 | Assert.Equal(diffs, result.ToList()); 52 | 53 | } 54 | [Fact] 55 | public void FromDelta_InputTooLong_Throws() 56 | { 57 | var delta = diffs.ToDelta(); 58 | var text1 = diffs.Text1() + "x"; 59 | Assert.Throws(() => 60 | DiffList.FromDelta(text1, delta).ToList() 61 | ); 62 | } 63 | 64 | [Fact] 65 | public void FromDelta_InvalidInput_Throws() 66 | { 67 | var delta = "=x"; 68 | Assert.Throws(() => 69 | DiffList.FromDelta("", delta).ToList() 70 | ); 71 | } 72 | [Fact] 73 | public void ToDelta_InputTooShort_Throws() 74 | { 75 | var delta = diffs.ToDelta(); 76 | var text1 = diffs.Text1()[1..]; 77 | Assert.Throws(() => 78 | DiffList.FromDelta(text1, delta).ToList() 79 | ); 80 | } 81 | 82 | [Fact] 83 | public void Delta_SpecialCharacters_Works() 84 | { 85 | var zero = (char)0; 86 | var one = (char)1; 87 | var two = (char)2; 88 | diffs = new List 89 | { 90 | Diff.Equal("\u0680 " + zero + " \t %"), 91 | Diff.Delete("\u0681 " + one + " \n ^"), 92 | Diff.Insert("\u0682 " + two + " \\ |") 93 | }; 94 | var text1 = diffs.Text1(); 95 | Assert.Equal("\u0680 " + zero + " \t %\u0681 " + one + " \n ^", text1); 96 | 97 | var delta = diffs.ToDelta(); 98 | // Lowercase, due to UrlEncode uses lower. 99 | Assert.Equal("=7\t-7\t+%da%82 %02 %5c %7c", delta); 100 | 101 | Assert.Equal(diffs, DiffList.FromDelta(text1, delta).ToList()); 102 | } 103 | 104 | 105 | [Fact] 106 | public void Delta_FromUnchangedCharacters_Succeeds() 107 | { 108 | // Verify pool of unchanged characters. 109 | var expected = new List 110 | { 111 | Diff.Insert("A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; / ? : @ & = + $ , # ") 112 | }; 113 | var text2 = expected.Text2(); 114 | Assert.Equal("A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", text2); 115 | 116 | var delta = expected.ToDelta(); 117 | Assert.Equal("+A-Z a-z 0-9 - _ . ! ~ * \' ( ) ; / ? : @ & = + $ , # ", delta); 118 | 119 | // Convert delta string into a diff. 120 | var actual = DiffList.FromDelta("", delta); 121 | Assert.Equal(expected, actual.ToList()); 122 | } 123 | 124 | [Fact] 125 | public void Delta_LargeString() 126 | { 127 | 128 | // 160 kb string. 129 | string a = "abcdefghij"; 130 | for (int i = 0; i < 14; i++) 131 | { 132 | a += a; 133 | } 134 | var diffs2 = new List { Diff.Insert(a) }; 135 | var delta = diffs2.ToDelta(); 136 | Assert.Equal("+" + a, delta); 137 | 138 | // Convert delta string into a diff. 139 | Assert.Equal(diffs2, DiffList.FromDelta("", delta).ToList()); 140 | 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/DiffMatchPatch.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | preview 6 | 7 | 8 | 1701;1702;IDE1006 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/Diff_ComputeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Text; 5 | using System.Threading; 6 | using Xunit; 7 | using static DiffMatchPatch.Diff; 8 | 9 | namespace DiffMatchPatch.Tests 10 | { 11 | 12 | public class Diff_ComputeTests 13 | { 14 | [Fact] 15 | public void DiffBetweenTwoEmptyStrings_IsEmpty() 16 | { 17 | var diffs = new List(); 18 | Assert.Equal(diffs, Diff.Compute("", "")); 19 | } 20 | [Fact] 21 | public void DiffBetweenTwoEqualStrings_IsOneEquality() 22 | { 23 | var expected1 = new List { Equal("abc") }; 24 | Assert.Equal(expected1, Diff.Compute("abc", "abc")); 25 | } 26 | [Fact] 27 | public void SimpleInsert() 28 | { 29 | var expected2 = new List { Equal("ab"), Insert("123"), Equal("c") }; 30 | Assert.Equal(expected2, Diff.Compute("abc", "ab123c")); 31 | } 32 | 33 | [Fact] 34 | public void SimpleDelete() 35 | { 36 | var expected3 = new List { Equal("a"), Delete("123"), Equal("bc") }; 37 | Assert.Equal(expected3, Diff.Compute("a123bc", "abc")); 38 | } 39 | 40 | [Fact] 41 | public void TwoInsertions() 42 | { 43 | var expected4 = new List 44 | { 45 | Equal("a"), 46 | Insert("123"), 47 | Equal("b"), 48 | Insert("456"), 49 | Equal("c") 50 | }; 51 | Assert.Equal(expected4, Diff.Compute("abc", "a123b456c")); 52 | 53 | } 54 | 55 | [Fact] 56 | public void TwoDeletes() 57 | { 58 | var expected5 = new List 59 | { 60 | Equal("a"), 61 | Delete("123"), 62 | Equal("b"), 63 | Delete("456"), 64 | Equal("c") 65 | }; 66 | Assert.Equal(expected5, Diff.Compute("a123b456c", "abc", 1f, false)); 67 | } 68 | 69 | [Fact] 70 | public void SimpleDeleteInsert_NoTimeout() 71 | { 72 | // Perform a real diff. 73 | // Switch off the timeout. 74 | var expected6 = new List { Delete("a"), Insert("b") }; 75 | Assert.Equal(expected6, Diff.Compute("a", "b", 0, false)); 76 | } 77 | 78 | [Fact] 79 | public void SentenceChange1() 80 | { 81 | var expected7 = new List 82 | { 83 | Delete("Apple"), 84 | Insert("Banana"), 85 | Equal("s are a"), 86 | Insert("lso"), 87 | Equal(" fruit.") 88 | }; 89 | Assert.Equal(expected7, Diff.Compute("Apples are a fruit.", "Bananas are also fruit.", 0, false)); 90 | } 91 | 92 | 93 | [Fact] 94 | public void SpecialCharacters_NoTimeout() 95 | { 96 | var expected8 = new List 97 | { 98 | Delete("a"), 99 | Insert("\u0680"), 100 | Equal("x"), 101 | Delete("\t"), 102 | Insert(new string(new char[] {(char) 0})) 103 | }; 104 | Assert.Equal(expected8, Diff.Compute("ax\t", "\u0680x" + (char)0, 0, false)); 105 | } 106 | 107 | 108 | [Fact] 109 | public void DiffWithOverlap1() 110 | { 111 | var expected9 = new List 112 | { 113 | Delete("1"), 114 | Equal("a"), 115 | Delete("y"), 116 | Equal("b"), 117 | Delete("2"), 118 | Insert("xab") 119 | }; 120 | Assert.Equal(expected9, Diff.Compute("1ayb2", "abxab", 0, false)); 121 | } 122 | 123 | 124 | [Fact] 125 | public void DiffWithOverlap2() 126 | { 127 | var expected10 = new List { Insert("xaxcx"), Equal("abc"), Delete("y") }; 128 | Assert.Equal(expected10, Diff.Compute("abcy", "xaxcxabc", 0, false)); 129 | } 130 | 131 | [Fact] 132 | public void DiffWithOverlap3() 133 | { 134 | var expected11 = new List 135 | { 136 | Delete("ABCD"), 137 | Equal("a"), 138 | Delete("="), 139 | Insert("-"), 140 | Equal("bcd"), 141 | Delete("="), 142 | Insert("-"), 143 | Equal("efghijklmnopqrs"), 144 | Delete("EFGHIJKLMNOefg") 145 | }; 146 | Assert.Equal(expected11, 147 | Diff.Compute("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg", "a-bcd-efghijklmnopqrs", 0, false)); 148 | } 149 | [Fact] 150 | public void LargeEquality() 151 | { 152 | var expected12 = new List 153 | { 154 | Insert(" "), 155 | Equal("a"), 156 | Insert("nd"), 157 | Equal(" [[Pennsylvania]]"), 158 | Delete(" and [[New") 159 | }; 160 | Assert.Equal(expected12, 161 | Diff.Compute("a [[Pennsylvania]] and [[New", " and [[Pennsylvania]]", 0, false)); 162 | } 163 | 164 | [Fact] 165 | public void Compute_WithHalfMatch() 166 | { 167 | var a = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, \r\nsed diam nonummy nibh euismod tincidunt ut laoreet dolore magna \r\naliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci \r\ntation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. \r\nDuis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie \r\nconsequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan\r\net iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore \r\nte feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil \r\nimperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; \r\nest usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores \r\nlegere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur\r\nmutationem consuetudium lectorum. Mirum est notare quam littera gothica, quam nunc putamus \r\nparum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta \r\ndecima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum."; 168 | var b = "Lorem ipsum dolor sit amet, adipiscing elit, \r\nsed diam nonummy nibh euismod tincidunt ut laoreet dolore vobiscum magna \r\naliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci \r\ntation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. \r\nDuis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie \r\nconsequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan\r\net iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore \r\nte feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil \r\nimperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; \r\nest usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores \r\nlegere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur\r\nmutationem consuetudium lectorum. Mirum est notare quam littera gothica, putamus \r\nparum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta \r\ndecima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum."; 169 | var collection = Diff.Compute(a,b, 5); 170 | var p = Patch.FromDiffs(collection); 171 | var result = p.Apply(a); 172 | Assert.Equal(b, result.Item1); 173 | } 174 | 175 | [Fact] 176 | public void Timeout() 177 | { 178 | var a = 179 | "`Twas brillig, and the slithy toves\nDid gyre and gimble in the wabe:\nAll mimsy were the borogoves,\nAnd the mome raths outgrabe.\n"; 180 | var b = 181 | "I am the very model of a modern major general,\nI've information vegetable, animal, and mineral,\nI know the kings of England, and I quote the fights historical,\nFrom Marathon to Waterloo, in order categorical.\n"; 182 | // Increase the text lengths by 1024 times to ensure a timeout. 183 | for (var x = 0; x < 10; x++) 184 | { 185 | a = a + a; 186 | b = b + b; 187 | } 188 | var timeout = TimeSpan.FromMilliseconds(100); 189 | 190 | using (var cts = new CancellationTokenSource(timeout)) 191 | { 192 | var stopWatch = Stopwatch.StartNew(); 193 | Diff.Compute(a, b, false, false, cts.Token); 194 | var elapsed = stopWatch.Elapsed; 195 | // assert that elapsed time is between timeout and 2*timeout (be forgiving) 196 | Assert.True(timeout <= elapsed.Add(TimeSpan.FromMilliseconds(1)), string.Format("Expected timeout < elapsed. Elapsed = {0}, Timeout = {1}.", elapsed, timeout)); 197 | Assert.True(TimeSpan.FromTicks(2 * timeout.Ticks) > elapsed); 198 | } 199 | } 200 | 201 | [Fact] 202 | public void SimpleLinemodeSpeedup() 203 | { 204 | var timeoutInSeconds4 = 0; 205 | 206 | // Test the linemode speedup. 207 | // Must be long to pass the 100 char cutoff. 208 | var a = 209 | "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; 210 | var b = 211 | "abcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\nabcdefghij\n"; 212 | Assert.Equal( 213 | Diff.Compute(a, b, timeoutInSeconds4, true), 214 | Diff.Compute(a, b, timeoutInSeconds4, false)); 215 | } 216 | 217 | [Fact] 218 | public void SingleLineModeSpeedup() 219 | { 220 | var a = "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; 221 | var b = "abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"; 222 | Assert.Equal(Diff.Compute(a, b, 0, true), Diff.Compute(a, b, 0, false)); 223 | } 224 | 225 | [Fact] 226 | public void OverlapLineMode() 227 | { 228 | var a = "1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n"; 229 | var b = "abcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n1234567890\n1234567890\n1234567890\nabcdefghij\n"; 230 | var textsLinemode = RebuildTexts(Diff.Compute(a, b, 0, true)); 231 | var textsTextmode = RebuildTexts(Diff.Compute(a, b, 0, false)); 232 | Assert.Equal(textsTextmode, textsLinemode); 233 | } 234 | 235 | private static Tuple RebuildTexts(IEnumerable diffs) 236 | { 237 | var text = Tuple.Create(new StringBuilder(), new StringBuilder()); 238 | foreach (var myDiff in diffs) 239 | { 240 | if (myDiff.Operation != Operation.Insert) 241 | { 242 | text.Item1.Append(myDiff.Text); 243 | } 244 | if (myDiff.Operation != Operation.Delete) 245 | { 246 | text.Item2.Append(myDiff.Text); 247 | } 248 | } 249 | return Tuple.Create(text.Item1.ToString(), text.Item2.ToString()); 250 | } 251 | } 252 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/HalfMatchResultTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace DiffMatchPatch.Tests 4 | { 5 | public class HalfMatchResultTests 6 | { 7 | [Fact] 8 | public void HalfMatchResult_Reverse_ReversesPrefixAndSuffix() 9 | { 10 | var r = -new HalfMatchResult("p1", "s1", "p2", "s2", "m"); 11 | Assert.Equal(new("p2", "s2", "p1", "s1", "m"), r); 12 | Assert.Equal(r, -(-r)); 13 | } 14 | [Fact] 15 | public void HalfMatchResult_IsEmpty_WhenCommonMiddleNotEmpty_ReturnsFalse() 16 | { 17 | var r = new HalfMatchResult("p1", "s1", "p2", "s2", "m"); 18 | Assert.False(r.IsEmpty); 19 | } 20 | [Fact] 21 | public void HalfMatchResult_IsEmpty_WhenCommonMiddleEmpty_ReturnsTrue() 22 | { 23 | var r = new HalfMatchResult("p1", "s1", "p2", "s2", ""); 24 | Assert.True(r.IsEmpty); 25 | } 26 | [Fact] 27 | public void HalfMatchResult_LargerThan_SmallerThan() 28 | { 29 | var r1 = HalfMatchResult.Empty with { CommonMiddle = "abc" }; 30 | var r2 = HalfMatchResult.Empty with { CommonMiddle = "abcd" }; 31 | Assert.True(r2 > r1); 32 | Assert.True(r1 < r2); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/OriginalTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using Original; 3 | 4 | namespace DiffMatchPatch.Original.Tests 5 | { 6 | 7 | public class OriginalTests 8 | { 9 | [Fact] 10 | public void AllOriginalTestsPass() 11 | { 12 | diff_match_patchTest.OriginalMain(null); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/TextUtil_CommonOverlapTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace DiffMatchPatch.Tests 4 | { 5 | 6 | public class TextUtilTests 7 | { 8 | [Fact] 9 | public void CommonOverlapEmptyStringNoOverlap() 10 | { 11 | // Detect any suffix/prefix overlap. 12 | // Null case. 13 | Assert.Equal(0, TextUtil.CommonOverlap("", "abcd")); 14 | } 15 | [Fact] 16 | public void CommonOverlapFirstIsPrefixOfSecondFullOverlap() 17 | { 18 | 19 | // Whole case. 20 | Assert.Equal(3, TextUtil.CommonOverlap("abc", "abcd")); 21 | } 22 | [Fact] 23 | public void CommonOverlapRecurringPatternOverlap() 24 | { 25 | 26 | Assert.Equal(4, TextUtil.CommonOverlap("xyz1212", "1212abc")); 27 | } 28 | [Fact] 29 | public void CommonOverlapDisjunctStringsNoOverlap() 30 | { 31 | 32 | // No overlap. 33 | Assert.Equal(0, TextUtil.CommonOverlap("123456", "abcd")); 34 | } 35 | [Fact] 36 | public void CommonOverlapPatternInTheMiddle_NoOverlap() 37 | { 38 | 39 | Assert.Equal(0, TextUtil.CommonOverlap("123456xxx", "efgxxxabcd")); 40 | } 41 | [Fact] 42 | public void CommonOverlapFirstEndsWithStartOfSecondOverlap() 43 | { 44 | 45 | // Overlap. 46 | Assert.Equal(3, TextUtil.CommonOverlap("123456xyz", "xyzabcd")); 47 | } 48 | [Fact] 49 | public void CommonOverlapUnicodeLigaturesAndComponentLettersNoOverlap() 50 | { 51 | // Unicode. 52 | // Some overly clever languages (C#) may treat ligatures as equal to their 53 | // component letters. E.g. U+FB01 == 'fi' 54 | Assert.Equal(0, TextUtil.CommonOverlap("fi", "\ufb01i")); 55 | } 56 | 57 | [Fact] 58 | public void CommonPrefixDisjunctStringsNoCommonPrefix() 59 | { 60 | // Detect any common suffix. 61 | // Null case. 62 | Assert.Equal(0, TextUtil.CommonPrefix("abc", "xyz")); 63 | } 64 | 65 | [Fact] 66 | public void CommonPrefixBothStringsStartWithSameCommonPrefixIsDetected() 67 | { 68 | // Non-null case. 69 | Assert.Equal(4, TextUtil.CommonPrefix("1234abcdef", "1234xyz")); 70 | } 71 | [Fact] 72 | public void CommonPrefixBothStringsStartWithSameCommonPrefixIsDetected2() 73 | { 74 | // Non-null case. 75 | Assert.Equal(4, TextUtil.CommonPrefix("abc1234abcdef", "efgh1234xyz", 3, 4)); 76 | } 77 | 78 | [Fact] 79 | public void CommonPrefixFirstStringIsSubstringOfSecondCommonPrefixIsDetected() 80 | { 81 | 82 | // Whole case. 83 | Assert.Equal(4, TextUtil.CommonPrefix("1234", "1234xyz")); 84 | } 85 | 86 | [Fact] 87 | public void CommonSuffixDisjunctStringsNoCommonSuffix() 88 | { 89 | // Detect any common suffix. 90 | // Null case. 91 | Assert.Equal(0, TextUtil.CommonSuffix("abc", "xyz")); 92 | } 93 | 94 | [Fact] 95 | public void CommonSuffixBothStringsEndWithSameCommonSuffixIsDetected() 96 | { 97 | // Non-null case. 98 | Assert.Equal(4, TextUtil.CommonSuffix("abcdef1234", "xyz1234")); 99 | } 100 | [Fact] 101 | public void CommonSuffixBothStringsEndWithSameCommonSuffixIsDetected2() 102 | { 103 | // Non-null case. 104 | Assert.Equal(4, TextUtil.CommonSuffix("abcdef1234abcd", "xyz1234efgh", 10, 7)); 105 | } 106 | 107 | [Fact] 108 | public void CommonSuffixFirstStringIsSubstringOfSecondCommonSuffixIsDetected() 109 | { 110 | // Whole case. 111 | Assert.Equal(4, TextUtil.CommonSuffix("1234", "xyz1234")); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/TextUtil_HalfMatchTests.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0057 2 | using System; 3 | using Xunit; 4 | 5 | namespace DiffMatchPatch.Tests 6 | { 7 | 8 | public class TextUtilHalfMatchTests 9 | { 10 | [Fact] 11 | public void WhenLeftIsEmptyReturnsEmpty() 12 | { 13 | var result = TextUtil.HalfMatch("", "12345"); 14 | Assert.True(result.IsEmpty); 15 | } 16 | 17 | [Fact] 18 | public void WhenRightIsEmptyReturnsEmpty() 19 | { 20 | var result = TextUtil.HalfMatch("12345", ""); 21 | Assert.True(result.IsEmpty); 22 | } 23 | 24 | [Fact] 25 | public void WhenTextDoesNotMatchReturnsNull() 26 | { 27 | // No match. 28 | var result = TextUtil.HalfMatch("1234567890", "abcdef"); 29 | Assert.True(result.IsEmpty); 30 | } 31 | 32 | [Fact] 33 | public void WhenSubstringIsLessThanHalfTheOriginalStringReturnsNull() 34 | { 35 | var result = TextUtil.HalfMatch("12345", "23"); 36 | Assert.True(result.IsEmpty); 37 | } 38 | 39 | [Fact] 40 | public void WhenSubstringIsMoreThanHalfTheOriginalStringReturnsResult1() 41 | { 42 | 43 | var result = TextUtil.HalfMatch("1234567890", "a345678z"); 44 | Assert.Equal(new HalfMatchResult("12", "90", "a", "z", "345678"), result); 45 | } 46 | 47 | [Fact] 48 | public void WhenSubstringIsMoreThanHalfTheOriginalStringReturnsResult2() 49 | { 50 | var result = TextUtil.HalfMatch("a345678z", "1234567890"); 51 | Assert.Equal(new HalfMatchResult("a", "z", "12", "90", "345678"), result); 52 | } 53 | 54 | [Fact] 55 | public void WhenSubstringIsMoreThanHalfTheOriginalStringReturnsResult3() 56 | { 57 | var result = TextUtil.HalfMatch("abc56789z", "1234567890"); 58 | Assert.Equal(new HalfMatchResult("abc", "z", "1234", "0", "56789"), result); 59 | 60 | } 61 | 62 | [Fact] 63 | public void WhenSubstringIsMoreThanHalfTheOriginalStringReturnsResult4() 64 | { 65 | var result = TextUtil.HalfMatch("a23456xyz", "1234567890"); 66 | Assert.Equal(new HalfMatchResult("a", "xyz", "1", "7890", "23456"), result); 67 | } 68 | 69 | [Fact] 70 | public void WhenSubstringIsMoreThanHalfTheOriginalStringMultipleMatchesReturnsLongestSubstring1() 71 | { 72 | 73 | var result = TextUtil.HalfMatch("121231234123451234123121", "a1234123451234z"); 74 | Assert.Equal(new HalfMatchResult("12123", "123121", "a", "z", "1234123451234"), result); 75 | 76 | } 77 | 78 | [Fact] 79 | public void WhenSubstringIsMoreThanHalfTheOriginalStringMultipleMatchesReturnsLongestSubstring2() 80 | { 81 | var result = TextUtil.HalfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-="); 82 | Assert.Equal(new HalfMatchResult("", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-="), result); 83 | } 84 | 85 | [Fact] 86 | public void WhenSubstringIsMoreThanHalfTheOriginalStringMultipleMatchesReturnsLongestSubstring3() 87 | { 88 | 89 | var result = TextUtil.HalfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy"); 90 | Assert.Equal(new HalfMatchResult("-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y"), result); 91 | } 92 | 93 | [Fact] 94 | public void WhenSubstringIsMoreThanHalfTheOriginalStringNonOptimal() 95 | { 96 | // Non-optimal halfmatch. 97 | // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy 98 | var result = TextUtil.HalfMatch("qHilloHelloHew", "xHelloHeHulloy"); 99 | Assert.Equal(new HalfMatchResult("qHillo", "w", "x", "Hulloy", "HelloHe"), result); 100 | } 101 | 102 | [Fact] 103 | public void diff_halfmatchTest() 104 | { 105 | // No match. 106 | Assert.Equal(new string[] { "a", "z", "12", "90", "345678" }, diff_halfMatch("a345678z", "1234567890")); 107 | 108 | Assert.Null(diff_halfMatch("1234567890", "abcdef")); 109 | 110 | Assert.Null(diff_halfMatch("12345", "23")); 111 | 112 | // Single Match. 113 | Assert.Equal(new string[] { "12", "90", "a", "z", "345678" }, diff_halfMatch("1234567890", "a345678z")); 114 | 115 | 116 | Assert.Equal(new string[] { "abc", "z", "1234", "0", "56789" }, diff_halfMatch("abc56789z", "1234567890")); 117 | 118 | Assert.Equal(new string[] { "a", "xyz", "1", "7890", "23456" }, diff_halfMatch("a23456xyz", "1234567890")); 119 | 120 | // Multiple Matches. 121 | Assert.Equal(new string[] { "12123", "123121", "a", "z", "1234123451234" }, diff_halfMatch("121231234123451234123121", "a1234123451234z")); 122 | 123 | Assert.Equal(new string[] { "", "-=-=-=-=-=", "x", "", "x-=-=-=-=-=-=-=" }, diff_halfMatch("x-=-=-=-=-=-=-=-=-=-=-=-=", "xx-=-=-=-=-=-=-=")); 124 | 125 | Assert.Equal(new string[] { "-=-=-=-=-=", "", "", "y", "-=-=-=-=-=-=-=y" }, diff_halfMatch("-=-=-=-=-=-=-=-=-=-=-=-=y", "-=-=-=-=-=-=-=yy")); 126 | 127 | // Non-optimal halfmatch. 128 | // Optimal diff would be -q+x=H-i+e=lloHe+Hu=llo-Hew+y not -qHillo+x=HelloHe-w+Hulloy 129 | Assert.Equal(new string[] { "qHillo", "w", "x", "Hulloy", "HelloHe" }, diff_halfMatch("qHilloHelloHew", "xHelloHeHulloy")); 130 | 131 | } 132 | 133 | 134 | protected string[] diff_halfMatch(string text1, string text2) 135 | { 136 | string longtext = text1.Length > text2.Length ? text1 : text2; 137 | string shorttext = text1.Length > text2.Length ? text2 : text1; 138 | if (longtext.Length < 4 || shorttext.Length * 2 < longtext.Length) 139 | { 140 | return null; // Pointless. 141 | } 142 | 143 | // First check if the second quarter is the seed for a half-match. 144 | string[] hm1 = diff_halfMatchI(longtext, shorttext, 145 | (longtext.Length + 3) / 4); 146 | // Check again based on the third quarter. 147 | string[] hm2 = diff_halfMatchI(longtext, shorttext, 148 | (longtext.Length + 1) / 2); 149 | string[] hm; 150 | if (hm1 == null && hm2 == null) 151 | { 152 | return null; 153 | } 154 | else if (hm2 == null) 155 | { 156 | hm = hm1; 157 | } 158 | else if (hm1 == null) 159 | { 160 | hm = hm2; 161 | } 162 | else 163 | { 164 | // Both matched. Select the longest. 165 | hm = hm1[4].Length > hm2[4].Length ? hm1 : hm2; 166 | } 167 | 168 | // A half-match was found, sort out the return data. 169 | if (text1.Length > text2.Length) 170 | { 171 | return hm; 172 | //return new string[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; 173 | } 174 | else 175 | { 176 | return new string[] { hm[2], hm[3], hm[0], hm[1], hm[4] }; 177 | } 178 | } 179 | 180 | /** 181 | * Does a Substring of shorttext exist within longtext such that the 182 | * Substring is at least half the length of longtext? 183 | * @param longtext Longer string. 184 | * @param shorttext Shorter string. 185 | * @param i Start index of quarter length Substring within longtext. 186 | * @return Five element string array, containing the prefix of longtext, the 187 | * suffix of longtext, the prefix of shorttext, the suffix of shorttext 188 | * and the common middle. Or null if there was no match. 189 | */ 190 | private string[] diff_halfMatchI(string longtext, string shorttext, int i) 191 | { 192 | // Start with a 1/4 length Substring at position i as a seed. 193 | string seed = longtext.Substring(i, longtext.Length / 4); 194 | int j = -1; 195 | string best_common = string.Empty; 196 | string best_longtext_a = string.Empty, best_longtext_b = string.Empty; 197 | string best_shorttext_a = string.Empty, best_shorttext_b = string.Empty; 198 | while (j < shorttext.Length && (j = shorttext.IndexOf(seed, j + 1, 199 | StringComparison.Ordinal)) != -1) 200 | { 201 | int prefixLength = diff_commonPrefix(longtext.Substring(i), 202 | shorttext.Substring(j)); 203 | int suffixLength = diff_commonSuffix(longtext.Substring(0, i), 204 | shorttext.Substring(0, j)); 205 | if (best_common.Length < suffixLength + prefixLength) 206 | { 207 | best_common = shorttext.Substring(j - suffixLength, suffixLength) 208 | + shorttext.Substring(j, prefixLength); 209 | best_longtext_a = longtext.Substring(0, i - suffixLength); 210 | best_longtext_b = longtext.Substring(i + prefixLength); 211 | best_shorttext_a = shorttext.Substring(0, j - suffixLength); 212 | best_shorttext_b = shorttext.Substring(j + prefixLength); 213 | } 214 | } 215 | if (best_common.Length * 2 >= longtext.Length) 216 | { 217 | return new string[]{best_longtext_a, best_longtext_b,best_shorttext_a, best_shorttext_b, best_common}; 218 | } 219 | else 220 | { 221 | return null; 222 | } 223 | } 224 | public int diff_commonSuffix(string text1, string text2) 225 | { 226 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 227 | int text1_length = text1.Length; 228 | int text2_length = text2.Length; 229 | int n = Math.Min(text1.Length, text2.Length); 230 | for (int i = 1; i <= n; i++) 231 | { 232 | if (text1[text1_length - i] != text2[text2_length - i]) 233 | { 234 | return i - 1; 235 | } 236 | } 237 | return n; 238 | } 239 | public int diff_commonPrefix(string text1, string text2) 240 | { 241 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 242 | int n = Math.Min(text1.Length, text2.Length); 243 | for (int i = 0; i < n; i++) 244 | { 245 | if (text1[i] != text2[i]) 246 | { 247 | return i; 248 | } 249 | } 250 | return n; 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/TextUtil_LinesToCharsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | using Xunit; 6 | 7 | namespace DiffMatchPatch.Tests 8 | { 9 | 10 | public class TextUtil_LinesToCharsTests 11 | { 12 | [Fact] 13 | public void Compress_ConvertsLinesToChars() 14 | { 15 | var compressor = new LineToCharCompressor(); 16 | var result1 = compressor.Compress("alpha\nbeta\nalpha\n"); 17 | var result2 = compressor.Compress("beta\nalpha\nbeta\n"); 18 | Assert.Equal("\u0000\u0001\u0000", result1); 19 | Assert.Equal("\u0001\u0000\u0001", result2); 20 | } 21 | 22 | [Fact] 23 | public void Compress_WhenCalled_EmptyText_ReturnsEmptyString() 24 | { 25 | var compressor = new LineToCharCompressor(); 26 | var result = compressor.Compress(string.Empty); 27 | Assert.Equal("", result); 28 | } 29 | 30 | [Fact] 31 | public void Compress_OneLine() 32 | { 33 | var d = new LineToCharCompressor(); 34 | var result = d.Compress("a"); 35 | Assert.Equal("\u0000", result); 36 | } 37 | 38 | [Fact] 39 | public void Compress_MultipleLines() 40 | { 41 | var d = new LineToCharCompressor(); 42 | var result = d.Compress("line1\r\nline2\r\n"); 43 | Assert.Equal("\u0000\u0001", result); 44 | } 45 | 46 | [Fact] 47 | public void Decompress() 48 | { 49 | var d = new LineToCharCompressor(); 50 | var input = "line1\r\nline2\r\n"; 51 | var compressed = d.Compress(input); 52 | var result = d.Decompress(compressed); 53 | Assert.Equal(input, result); 54 | } 55 | 56 | [Fact] 57 | public void Decompress_OneLine() 58 | { 59 | var compressor = new LineToCharCompressor(); 60 | var result = compressor.Compress("a"); 61 | Assert.Equal("\u0000", result); 62 | } 63 | 64 | [Fact] 65 | public void Compress_DisjunctStrings() 66 | { 67 | var compressor = new LineToCharCompressor(); 68 | var result1 = compressor.Compress("a"); 69 | var result2 = compressor.Compress("b"); 70 | 71 | Assert.Equal("\u0000", result1); 72 | Assert.Equal("\u0001", result2); 73 | } 74 | 75 | [Fact] 76 | public void Compress_MoreThan300Entries() 77 | { 78 | // More than 256 to reveal any 8-bit limitations. 79 | var n = 300; 80 | var lineList = new StringBuilder(); 81 | var charList = new StringBuilder(); 82 | for (var x = 0; x < n; x++) 83 | { 84 | lineList.Append(x + "\n"); 85 | charList.Append(Convert.ToChar(x)); 86 | } 87 | 88 | var lines = lineList.ToString(); 89 | var chars = charList.ToString(); 90 | Assert.Equal(n, chars.Length); 91 | 92 | var compressor = new LineToCharCompressor(); 93 | var result = compressor.Compress(lines); 94 | 95 | Assert.Equal(chars, result); 96 | } 97 | 98 | [Fact] 99 | public void Compress_MoreThan65535Lines_DecompressesCorrectly() 100 | { 101 | // More than 65536 to verify any 16-bit limitation. 102 | var lineList = new StringBuilder(); 103 | for (int i = 0; i < 66000; i++) 104 | { 105 | lineList.Append(i + "\n"); 106 | } 107 | var chars = lineList.ToString(); 108 | 109 | LineToCharCompressor compressor = new LineToCharCompressor(); 110 | 111 | var result = compressor.Compress(chars, sizeof(char)); 112 | var decompressed = compressor.Decompress(result); 113 | 114 | Assert.Equal(chars, decompressed); 115 | } 116 | 117 | [Fact] 118 | public void MultipleTexts() 119 | { 120 | var text1 = Enumerable.Range(1, 70000).Aggregate(new StringBuilder(), (sb, i) => sb.Append(i).AppendLine()).ToString(); 121 | var text2 = Enumerable.Range(20000, 999999).Aggregate(new StringBuilder(), (sb, i) => sb.Append(i).AppendLine()).ToString(); 122 | 123 | var compressor = new LineToCharCompressor(); 124 | 125 | var compressed1 = compressor.Compress(text1, 40000); 126 | var compressed2 = compressor.Compress(text2); 127 | 128 | var decompressed1 = compressor.Decompress(compressed1); 129 | var decompressed2 = compressor.Decompress(compressed2); 130 | 131 | Assert.Equal(text1, decompressed1); 132 | Assert.Equal(text2, decompressed2); 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /DiffMatchPatch.Tests/TextUtil_MatchPatternTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008 Google Inc. All Rights Reserved. 3 | * Author: fraser@google.com (Neil Fraser) 4 | * Author: anteru@developer.shelter13.net (Matthaeus G. Chajdas) 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * Diff Match and Patch -- Test Harness 19 | * http://code.google.com/p/google-diff-match-patch/ 20 | */ 21 | 22 | using Xunit; 23 | 24 | namespace DiffMatchPatch.Tests 25 | { 26 | 27 | public class TextUtil_MatchPatternTests 28 | { 29 | [Fact] 30 | public void EqualStrings_FullMatch() 31 | { 32 | Assert.Equal(0, "abcdef".FindBestMatchIndex("abcdef", 1000)); 33 | } 34 | 35 | [Fact] 36 | public void EmptyString_NoMatch() 37 | { 38 | Assert.Equal(-1, "".FindBestMatchIndex("abcdef", 1)); 39 | } 40 | 41 | [Fact] 42 | public void EmptyPattern() 43 | { 44 | Assert.Equal(3, "abcdef".FindBestMatchIndex("", 3)); 45 | } 46 | 47 | [Fact] 48 | public void ExactMatch() 49 | { 50 | Assert.Equal(3, "abcdef".FindBestMatchIndex("de", 3)); 51 | } 52 | [Fact] 53 | public void MatchBeyondEnd() 54 | { 55 | Assert.Equal(3, "abcdef".FindBestMatchIndex("defy", 4)); 56 | } 57 | [Fact] 58 | public void OversizedPattern() 59 | { 60 | Assert.Equal(0, "abcdef".FindBestMatchIndex("abcdefy", 0)); 61 | } 62 | 63 | [Fact] 64 | public void ComplexMatch() 65 | { 66 | var input = "I am the very model of a modern major general."; 67 | var match = input.FindBestMatchIndex(" that berry ", 5, new MatchSettings(0.7f, 1000)); 68 | Assert.Equal(4, match); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /DiffMatchPatch.lutconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 180000 6 | -------------------------------------------------------------------------------- /DiffMatchPatch.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30428.66 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiffMatchPatch", "DiffMatchPatch\DiffMatchPatch.csproj", "{C6A55730-6B58-4D7E-AE08-1476EBD8419A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiffMatchPatch.Tests", "DiffMatchPatch.Tests\DiffMatchPatch.Tests.csproj", "{FE4EAA7A-2966-4AD7-84DF-1FCDD6C3D4C5}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiffMatchPatch.PerformanceTest", "DiffMatchPatch.PerformanceTest\DiffMatchPatch.PerformanceTest.csproj", "{3BAE9782-26E1-4936-97D7-B5DCEC8F3B22}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsole", "TestConsole\TestConsole.csproj", "{13F1A296-35DA-4D31-97E5-AFA05FF168BD}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {C6A55730-6B58-4D7E-AE08-1476EBD8419A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {C6A55730-6B58-4D7E-AE08-1476EBD8419A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {C6A55730-6B58-4D7E-AE08-1476EBD8419A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {C6A55730-6B58-4D7E-AE08-1476EBD8419A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {FE4EAA7A-2966-4AD7-84DF-1FCDD6C3D4C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {FE4EAA7A-2966-4AD7-84DF-1FCDD6C3D4C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {FE4EAA7A-2966-4AD7-84DF-1FCDD6C3D4C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {FE4EAA7A-2966-4AD7-84DF-1FCDD6C3D4C5}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {3BAE9782-26E1-4936-97D7-B5DCEC8F3B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {3BAE9782-26E1-4936-97D7-B5DCEC8F3B22}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {3BAE9782-26E1-4936-97D7-B5DCEC8F3B22}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {3BAE9782-26E1-4936-97D7-B5DCEC8F3B22}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {13F1A296-35DA-4D31-97E5-AFA05FF168BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {13F1A296-35DA-4D31-97E5-AFA05FF168BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {13F1A296-35DA-4D31-97E5-AFA05FF168BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {13F1A296-35DA-4D31-97E5-AFA05FF168BD}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {E2AD8194-10D6-4F86-8BD2-B0D00A6FE7C5} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /DiffMatchPatch/BitapAlgorithm.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | /* 4 | * https://en.wikipedia.org/wiki/Bitap_algorithm 5 | */ 6 | 7 | /// 8 | /// Implements the Bitap algorithm, a text search algorithm that allows for approximate string matching. 9 | /// This class provides functionality to locate the best instance of a given pattern within a text string, 10 | /// accounting for potential mismatches and errors. The algorithm is configured through MatchSettings which 11 | /// includes match threshold and distance, determining the sensitivity and flexibility of the search. 12 | /// 13 | internal class BitapAlgorithm(MatchSettings settings) 14 | { 15 | // Cost of an empty edit operation in terms of edit characters. 16 | // At what point is no match declared (0.0 = perfection, 1.0 = very loose). 17 | readonly float _matchThreshold = settings.MatchThreshold; 18 | // How far to search for a match (0 = exact location, 1000+ = broad match). 19 | // A match this many characters away from the expected location will add 20 | // 1.0 to the score (0.0 is a perfect match). 21 | readonly int _matchDistance = settings.MatchDistance; 22 | 23 | /// 24 | /// Locate the best instance of 'pattern' in 'text' near 'loc' using the 25 | /// Bitap algorithm. Returns -1 if no match found. 26 | /// 27 | /// The text to search. 28 | /// The pattern to search for. 29 | /// The location to search around. 30 | /// Best match index or -1. 31 | public int Match(string text, string pattern, int startIndex) 32 | { 33 | // Highest score beyond which we give up. 34 | double scoreThreshold = _matchThreshold; 35 | 36 | // Is there a nearby exact match? (speedup) 37 | var bestMatchIndex = text.IndexOf(pattern, startIndex, StringComparison.Ordinal); 38 | if (bestMatchIndex != -1) 39 | { 40 | scoreThreshold = Math.Min(MatchBitapScore(0, bestMatchIndex, startIndex, pattern), scoreThreshold); 41 | // What about in the other direction? (speedup) 42 | bestMatchIndex = text.LastIndexOf(pattern, 43 | Math.Min(startIndex + pattern.Length, text.Length), 44 | StringComparison.Ordinal); 45 | if (bestMatchIndex != -1) 46 | { 47 | scoreThreshold = Math.Min(MatchBitapScore(0, bestMatchIndex, startIndex, pattern), scoreThreshold); 48 | } 49 | } 50 | 51 | // Initialise the alphabet. 52 | var s = InitAlphabet(pattern); 53 | 54 | // Initialise the bit arrays. 55 | var matchmask = 1 << (pattern.Length - 1); 56 | bestMatchIndex = -1; 57 | 58 | int currentMinRange, currentMidpoint; 59 | var currentMaxRange = pattern.Length + text.Length; 60 | var lastComputedRow = Array.Empty(); 61 | for (var d = 0; d < pattern.Length; d++) 62 | { 63 | // Scan for the best match; each iteration allows for one more error. 64 | // Run a binary search to determine how far from 'loc' we can stray at 65 | // this error level. 66 | currentMinRange = 0; 67 | currentMidpoint = currentMaxRange; 68 | while (currentMinRange < currentMidpoint) 69 | { 70 | if (MatchBitapScore(d, startIndex + currentMidpoint, startIndex, pattern) <= scoreThreshold) 71 | currentMinRange = currentMidpoint; 72 | else 73 | currentMaxRange = currentMidpoint; 74 | currentMidpoint = (currentMaxRange - currentMinRange) / 2 + currentMinRange; 75 | } 76 | // Use the result from this iteration as the maximum for the next. 77 | currentMaxRange = currentMidpoint; 78 | var start = Math.Max(1, startIndex - currentMidpoint + 1); 79 | var finish = Math.Min(startIndex + currentMidpoint, text.Length) + pattern.Length; 80 | 81 | var rd = new int[finish + 2]; 82 | rd[finish + 1] = (1 << d) - 1; 83 | for (var j = finish; j >= start; j--) 84 | { 85 | int charMatch; 86 | if (text.Length <= j - 1 || !s.ContainsKey(text[j - 1])) 87 | // Out of range. 88 | charMatch = 0; 89 | else 90 | charMatch = s[text[j - 1]]; 91 | 92 | if (d == 0) 93 | // First pass: exact match. 94 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; 95 | else 96 | // Subsequent passes: fuzzy match. 97 | rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | ((lastComputedRow[j + 1] | lastComputedRow[j]) << 1) | 1 | lastComputedRow[j + 1]; 98 | 99 | if ((rd[j] & matchmask) != 0) 100 | { 101 | var score = MatchBitapScore(d, j - 1, startIndex, pattern); 102 | // This match will almost certainly be better than any existing 103 | // match. But check anyway. 104 | if (score <= scoreThreshold) 105 | { 106 | // Told you so. 107 | scoreThreshold = score; 108 | bestMatchIndex = j - 1; 109 | if (bestMatchIndex > startIndex) 110 | { 111 | // When passing loc, don't exceed our current distance from loc. 112 | start = Math.Max(1, 2 * startIndex - bestMatchIndex); 113 | } 114 | else 115 | { 116 | // Already passed loc, downhill from here on in. 117 | break; 118 | } 119 | } 120 | } 121 | } 122 | if (MatchBitapScore(d + 1, startIndex, startIndex, pattern) > scoreThreshold) 123 | { 124 | // No hope for a (better) match at greater error levels. 125 | break; 126 | } 127 | lastComputedRow = rd; 128 | } 129 | return bestMatchIndex; 130 | } 131 | 132 | /// 133 | /// Initialise the alphabet for the Bitap algorithm. 134 | /// 135 | /// 136 | /// 137 | internal static Dictionary InitAlphabet(string pattern) 138 | => pattern 139 | .Select((c, i) => (c, i)) 140 | .Aggregate(new Dictionary(), (d, x) => 141 | { 142 | d[x.c] = d.GetValueOrDefault(x.c) | (1 << (pattern.Length - x.i - 1)); 143 | return d; 144 | }); 145 | 146 | /// 147 | /// Compute and return the score for a match with e errors and x location. 148 | /// 149 | /// Number of errors in match. 150 | /// Location of match. 151 | /// Expected location of match. 152 | /// Pattern being sought. 153 | /// Overall score for match (0.0 = good, 1.0 = bad). 154 | private double MatchBitapScore(int errors, int location, int expectedLocation, string pattern) 155 | { 156 | var accuracy = (float)errors / pattern.Length; 157 | var proximity = Math.Abs(expectedLocation - location); 158 | 159 | return (_matchDistance, proximity) switch 160 | { 161 | (0, 0) => accuracy, 162 | (0, _) => 1.0, 163 | _ => accuracy + proximity / (float)_matchDistance 164 | }; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /DiffMatchPatch/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | internal static class Constants 4 | { 5 | // The number of bits in an int. 6 | public const short MatchMaxBits = 32; 7 | } 8 | -------------------------------------------------------------------------------- /DiffMatchPatch/Diff.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | public readonly record struct Diff(Operation Operation, string Text) 4 | { 5 | internal static Diff Create(Operation operation, string text) => new(operation, text); 6 | public static Diff Equal(ReadOnlySpan text) => Create(Operation.Equal, text.ToString()); 7 | public static Diff Insert(ReadOnlySpan text) => Create(Operation.Insert, text.ToString()); 8 | public static Diff Delete(ReadOnlySpan text) => Create(Operation.Delete, text.ToString()); 9 | public static Diff Empty => new(Operation.Equal, string.Empty); 10 | /// 11 | /// Generate a human-readable version of this Diff. 12 | /// 13 | /// 14 | public override string ToString() 15 | { 16 | var prettyText = Text.Replace('\n', '\u00b6'); 17 | return "Diff(" + Operation + ",\"" + prettyText + "\")"; 18 | } 19 | 20 | internal Diff Replace(string text) => this with { Text = text }; 21 | internal Diff Append(string text) => this with { Text = Text + text }; 22 | internal Diff Prepend(string text) => this with { Text = text + Text }; 23 | 24 | public bool IsEmpty => Text.Length == 0; 25 | 26 | /// 27 | /// Find the differences between two texts. 28 | /// 29 | /// Old string to be diffed 30 | /// New string to be diffed 31 | /// if specified, certain optimizations may be enabled to meet the time constraint, possibly resulting in a less optimal diff 32 | /// If false, then don't run a line-level diff first to identify the changed areas. If true, then run a faster slightly less optimal diff. 33 | /// 34 | public static ImmutableList Compute(string text1, string text2, float timeoutInSeconds = 0f, bool checklines = true) 35 | { 36 | using var cts = timeoutInSeconds <= 0 37 | ? new CancellationTokenSource() 38 | : new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)); 39 | return Compute(text1, text2, checklines, timeoutInSeconds > 0, cts.Token); 40 | } 41 | 42 | public static ImmutableList Compute(string text1, string text2, bool checkLines, bool optimizeForSpeed, CancellationToken token) 43 | => DiffAlgorithm.Compute(text1, text2, checkLines, optimizeForSpeed, token).ToImmutableList(); 44 | 45 | public bool IsLargeDelete(int size) => Operation == Operation.Delete && Text.Length > size; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /DiffMatchPatch/DiffAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using static DiffMatchPatch.Operation; 2 | 3 | namespace DiffMatchPatch; 4 | 5 | static class DiffAlgorithm 6 | { 7 | 8 | /// 9 | /// Find the differences between two texts. Simplifies the problem by 10 | /// stripping any common prefix or suffix off the texts before diffing. 11 | /// 12 | /// Old string to be diffed. 13 | /// New string to be diffed. 14 | /// Speedup flag. If false, then don't run a line-level diff first to identify the changed areas. If true, then run a faster slightly less optimal diff. 15 | /// Should optimizations be enabled? 16 | /// Cancellation token for cooperative cancellation 17 | /// 18 | internal static IEnumerable Compute(ReadOnlySpan text1, ReadOnlySpan text2, bool checklines, bool optimizeForSpeed, CancellationToken token) 19 | { 20 | if (text1.Length == text2.Length && text1.Length == 0) 21 | return Enumerable.Empty(); 22 | 23 | var commonlength = TextUtil.CommonPrefix(text1, text2); 24 | 25 | if (commonlength == text1.Length && commonlength == text2.Length) 26 | { 27 | // equal 28 | return new List() 29 | { 30 | Diff.Equal(text1) 31 | }; 32 | } 33 | 34 | // Trim off common prefix (speedup). 35 | var commonprefix = text1.Slice(0, commonlength); 36 | text1 = text1[commonlength..]; 37 | text2 = text2[commonlength..]; 38 | 39 | // Trim off common suffix (speedup). 40 | commonlength = TextUtil.CommonSuffix(text1, text2); 41 | var commonsuffix = text1[^commonlength..]; 42 | text1 = text1.Slice(0, text1.Length - commonlength); 43 | text2 = text2.Slice(0, text2.Length - commonlength); 44 | 45 | List diffs = new(); 46 | // Compute the diff on the middle block. 47 | if (commonprefix.Length != 0) 48 | { 49 | diffs.Insert(0, Diff.Equal(commonprefix)); 50 | } 51 | 52 | diffs.AddRange(ComputeImpl(text1, text2, checklines, optimizeForSpeed, token)); 53 | 54 | if (commonsuffix.Length != 0) 55 | { 56 | diffs.Add(Diff.Equal(commonsuffix)); 57 | } 58 | 59 | return diffs.CleanupMerge(); 60 | } 61 | 62 | /// 63 | /// Find the differences between two texts. Assumes that the texts do not 64 | /// have any common prefix or suffix. 65 | /// 66 | /// Old string to be diffed. 67 | /// New string to be diffed. 68 | /// Speedup flag. If false, then don't run a line-level diff first to identify the changed areas. If true, then run a faster slightly less optimal diff. 69 | /// Should optimizations be enabled? 70 | /// Cancellation token for cooperative cancellation 71 | /// 72 | private static IEnumerable ComputeImpl( 73 | ReadOnlySpan text1, 74 | ReadOnlySpan text2, 75 | bool checklines, 76 | bool optimizeForSpeed, 77 | CancellationToken token) 78 | { 79 | 80 | if (text1.Length == 0) 81 | { 82 | // Just add some text (speedup). 83 | return Diff.Insert(text2).ItemAsEnumerable(); 84 | } 85 | 86 | if (text2.Length == 0) 87 | { 88 | // Just delete some text (speedup). 89 | return Diff.Delete(text1).ItemAsEnumerable(); 90 | } 91 | 92 | var longtext = text1.Length > text2.Length ? text1 : text2; 93 | var shorttext = text1.Length > text2.Length ? text2 : text1; 94 | var i = longtext.IndexOf(shorttext, StringComparison.Ordinal); 95 | if (i != -1) 96 | { 97 | // Shorter text is inside the longer text (speedup). 98 | if (text1.Length > text2.Length) 99 | { 100 | return new[] 101 | { 102 | Diff.Delete(longtext.Slice(0, i)), 103 | Diff.Equal(shorttext), 104 | Diff.Delete(longtext[(i + shorttext.Length)..]) 105 | }; 106 | } 107 | else 108 | { 109 | return new[] 110 | { 111 | Diff.Insert(longtext.Slice(0, i)), 112 | Diff.Equal(shorttext), 113 | Diff.Insert(longtext[(i + shorttext.Length)..]) 114 | }; 115 | } 116 | } 117 | 118 | if (shorttext.Length == 1) 119 | { 120 | // Single character string. 121 | // After the previous speedup, the character can't be an equality. 122 | return new[] 123 | { 124 | Diff.Delete(text1), 125 | Diff.Insert(text2) 126 | }; 127 | } 128 | 129 | // Don't risk returning a non-optimal diff if we have unlimited time. 130 | if (optimizeForSpeed) 131 | { 132 | // Check to see if the problem can be split in two. 133 | var result = TextUtil.HalfMatch(text1, text2); 134 | if (!result.IsEmpty) 135 | { 136 | // A half-match was found, sort out the return data. 137 | // Send both pairs off for separate processing. 138 | var diffsA = Compute(result.Prefix1, result.Prefix2, checklines, optimizeForSpeed, token); 139 | var diffsB = Compute(result.Suffix1, result.Suffix2, checklines, optimizeForSpeed, token); 140 | 141 | // Merge the results. 142 | return diffsA 143 | .Concat(Diff.Equal(result.CommonMiddle)) 144 | .Concat(diffsB); 145 | } 146 | } 147 | if (checklines && text1.Length > 100 && text2.Length > 100) 148 | { 149 | return LineDiff(text1, text2, optimizeForSpeed, token); 150 | } 151 | 152 | return MyersDiffBisect(text1, text2, optimizeForSpeed, token); 153 | } 154 | 155 | /// 156 | /// Do a quick line-level diff on both strings, then rediff the parts for 157 | /// greater accuracy. This speedup can produce non-minimal Diffs. 158 | /// 159 | /// 160 | /// 161 | /// 162 | /// 163 | /// 164 | private static List LineDiff(ReadOnlySpan text1, ReadOnlySpan text2, bool optimizeForSpeed, CancellationToken token) 165 | { 166 | // Scan the text on a line-by-line basis first. 167 | var compressor = new LineToCharCompressor(); 168 | text1 = compressor.Compress(text1, char.MaxValue * 2 / 3); 169 | text2 = compressor.Compress(text2, char.MaxValue); 170 | var diffs = Compute(text1, text2, false, optimizeForSpeed, token) 171 | .Select(diff => diff.Replace(compressor.Decompress(diff.Text))) 172 | .ToList() 173 | .CleanupSemantic(); // Eliminate freak matches (e.g. blank lines) 174 | 175 | return RediffAfterLineDiff(diffs, optimizeForSpeed, token).ToList(); 176 | } 177 | 178 | // Rediff any replacement blocks, this time character-by-character. 179 | private static IEnumerable RediffAfterLineDiff(IEnumerable diffs, bool optimizeForSpeed, CancellationToken token) 180 | { 181 | var ins = new StringBuilder(); 182 | var del = new StringBuilder(); 183 | foreach (var diff in diffs.Concat(Diff.Empty)) 184 | { 185 | (ins, del) = diff.Operation switch 186 | { 187 | Insert => (ins.Append(diff.Text), del), 188 | Delete => (ins, del.Append(diff.Text)), 189 | _ => (ins, del) 190 | }; 191 | 192 | if (diff.Operation != Equal) 193 | { 194 | continue; 195 | } 196 | 197 | var consolidatedDiffsBeforeEqual = diff.Operation switch 198 | { 199 | Equal when ins.Length > 0 && del.Length > 0 => Compute(del.ToString(), ins.ToString(), false, optimizeForSpeed, token), 200 | Equal when del.Length > 0 => Diff.Delete(del.ToString()).ItemAsEnumerable(), 201 | Equal when ins.Length > 0 => Diff.Insert(ins.ToString()).ItemAsEnumerable(), 202 | _ => Enumerable.Empty() 203 | }; 204 | 205 | foreach (var d in consolidatedDiffsBeforeEqual) 206 | { 207 | yield return d; 208 | } 209 | 210 | if (!diff.IsEmpty) 211 | yield return diff; 212 | 213 | ins.Clear(); 214 | del.Clear(); 215 | } 216 | } 217 | 218 | /// 219 | /// Find the 'middle snake' of a diff, split the problem in two 220 | /// and return the recursively constructed diff. 221 | /// See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 222 | /// 223 | /// 224 | /// 225 | /// 226 | /// 227 | /// 228 | internal static IEnumerable MyersDiffBisect(ReadOnlySpan text1, ReadOnlySpan text2, bool optimizeForSpeed, CancellationToken token) 229 | { 230 | // Cache the text lengths to prevent multiple calls. 231 | var text1Length = text1.Length; 232 | var text2Length = text2.Length; 233 | var maxD = (text1Length + text2Length + 1) / 2; 234 | var vOffset = maxD; 235 | var vLength = 2 * maxD; 236 | var v1 = new int[vLength]; 237 | var v2 = new int[vLength]; 238 | for (var x = 0; x < vLength; x++) 239 | { 240 | v1[x] = -1; 241 | } 242 | for (var x = 0; x < vLength; x++) 243 | { 244 | v2[x] = -1; 245 | } 246 | v1[vOffset + 1] = 0; 247 | v2[vOffset + 1] = 0; 248 | var delta = text1Length - text2Length; 249 | // If the total number of characters is odd, then the front path will 250 | // collide with the reverse path. 251 | var front = delta % 2 != 0; 252 | // Offsets for start and end of k loop. 253 | // Prevents mapping of space beyond the grid. 254 | var k1Start = 0; 255 | var k1End = 0; 256 | var k2Start = 0; 257 | var k2End = 0; 258 | for (var d = 0; d < maxD; d++) 259 | { 260 | // Bail out if cancelled. 261 | if (token.IsCancellationRequested) 262 | { 263 | break; 264 | } 265 | 266 | // Walk the front path one step. 267 | for (var k1 = -d + k1Start; k1 <= d - k1End; k1 += 2) 268 | { 269 | var k1Offset = vOffset + k1; 270 | int x1; 271 | if (k1 == -d || k1 != d && v1[k1Offset - 1] < v1[k1Offset + 1]) 272 | { 273 | x1 = v1[k1Offset + 1]; 274 | } 275 | else 276 | { 277 | x1 = v1[k1Offset - 1] + 1; 278 | } 279 | var y1 = x1 - k1; 280 | while (x1 < text1Length && y1 < text2Length 281 | && text1[x1] == text2[y1]) 282 | { 283 | x1++; 284 | y1++; 285 | } 286 | v1[k1Offset] = x1; 287 | if (x1 > text1Length) 288 | { 289 | // Ran off the right of the graph. 290 | k1End += 2; 291 | } 292 | else if (y1 > text2Length) 293 | { 294 | // Ran off the bottom of the graph. 295 | k1Start += 2; 296 | } 297 | else if (front) 298 | { 299 | var k2Offset = vOffset + delta - k1; 300 | if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] != -1) 301 | { 302 | // Mirror x2 onto top-left coordinate system. 303 | var x2 = text1Length - v2[k2Offset]; 304 | if (x1 >= x2) 305 | { 306 | // Overlap detected. 307 | return BisectSplit(text1, text2, x1, y1, optimizeForSpeed, token); 308 | } 309 | } 310 | } 311 | } 312 | 313 | // Walk the reverse path one step. 314 | for (var k2 = -d + k2Start; k2 <= d - k2End; k2 += 2) 315 | { 316 | var k2Offset = vOffset + k2; 317 | int x2; 318 | if (k2 == -d || k2 != d && v2[k2Offset - 1] < v2[k2Offset + 1]) 319 | { 320 | x2 = v2[k2Offset + 1]; 321 | } 322 | else 323 | { 324 | x2 = v2[k2Offset - 1] + 1; 325 | } 326 | var y2 = x2 - k2; 327 | while (x2 < text1Length && y2 < text2Length 328 | && text1[text1Length - x2 - 1] 329 | == text2[text2Length - y2 - 1]) 330 | { 331 | x2++; 332 | y2++; 333 | } 334 | v2[k2Offset] = x2; 335 | if (x2 > text1Length) 336 | { 337 | // Ran off the left of the graph. 338 | k2End += 2; 339 | } 340 | else if (y2 > text2Length) 341 | { 342 | // Ran off the top of the graph. 343 | k2Start += 2; 344 | } 345 | else if (!front) 346 | { 347 | var k1Offset = vOffset + delta - k2; 348 | if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] != -1) 349 | { 350 | var x1 = v1[k1Offset]; 351 | var y1 = vOffset + x1 - k1Offset; 352 | // Mirror x2 onto top-left coordinate system. 353 | x2 = text1Length - v2[k2Offset]; 354 | if (x1 >= x2) 355 | { 356 | // Overlap detected. 357 | return BisectSplit(text1, text2, x1, y1, optimizeForSpeed, token); 358 | } 359 | } 360 | } 361 | } 362 | } 363 | // Diff took too long and hit the deadline or 364 | // number of Diffs equals number of characters, no commonality at all. 365 | return new[] { Diff.Delete(text1), Diff.Insert(text2) }; 366 | } 367 | 368 | /// 369 | /// Given the location of the 'middle snake', split the diff in two parts 370 | /// and recurse. 371 | /// 372 | /// 373 | /// 374 | /// Index of split point in text1. 375 | /// Index of split point in text2. 376 | /// 377 | /// 378 | /// 379 | private static IEnumerable BisectSplit(ReadOnlySpan text1, ReadOnlySpan text2, int x, int y, bool optimizeForSpeed, CancellationToken token) 380 | { 381 | var text1A = text1.Slice(0, x); 382 | var text2A = text2.Slice(0, y); 383 | var text1B = text1[x..]; 384 | var text2B = text2[y..]; 385 | 386 | // Compute both Diffs serially. 387 | var diffsa = Compute(text1A, text2A, false, optimizeForSpeed, token); 388 | var diffsb = Compute(text1B, text2B, false, optimizeForSpeed, token); 389 | 390 | return diffsa.Concat(diffsb); 391 | } 392 | 393 | } 394 | -------------------------------------------------------------------------------- /DiffMatchPatch/DiffListBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | internal static class DiffListBuilder 4 | { 5 | /// 6 | /// Increase the context until it is unique, 7 | /// but don't let the pattern expand beyond Match_MaxBits. 8 | /// Source text 9 | /// 10 | internal static (int start1, int length1, int start2, int length2) AddContext( 11 | this ImmutableList.Builder diffListBuilder, 12 | string text, int start1, int length1, int start2, int length2, short patchMargin = 4) 13 | { 14 | if (text.Length == 0) 15 | { 16 | return (start1, length1, start2, length2); 17 | } 18 | 19 | var pattern = text.Substring(start2, length1); 20 | var padding = 0; 21 | 22 | // Look for the first and last matches of pattern in text. If two 23 | // different matches are found, increase the pattern length. 24 | while (text.IndexOf(pattern, StringComparison.Ordinal) 25 | != text.LastIndexOf(pattern, StringComparison.Ordinal) 26 | && pattern.Length < Constants.MatchMaxBits - patchMargin - patchMargin) 27 | { 28 | padding += patchMargin; 29 | var begin = Math.Max(0, start2 - padding); 30 | pattern = text[begin..Math.Min(text.Length, start2 + length1 + padding)]; 31 | } 32 | // Add one chunk for good luck. 33 | padding += patchMargin; 34 | 35 | // Add the prefix. 36 | var begin1 = Math.Max(0, start2 - padding); 37 | var prefix = text[begin1..start2]; 38 | if (prefix.Length != 0) 39 | { 40 | diffListBuilder.Insert(0, Diff.Equal(prefix)); 41 | } 42 | // Add the suffix. 43 | var begin2 = start2 + length1; 44 | var length = Math.Min(text.Length, start2 + length1 + padding) - begin2; 45 | var suffix = text.Substring(begin2, length); 46 | if (suffix.Length != 0) 47 | { 48 | diffListBuilder.Add(Diff.Equal(suffix)); 49 | } 50 | 51 | // Roll back the start points. 52 | start1 -= prefix.Length; 53 | start2 -= prefix.Length; 54 | // Extend the lengths. 55 | length1 = length1 + prefix.Length + suffix.Length; 56 | length2 = length2 + prefix.Length + suffix.Length; 57 | 58 | return (start1, length1, start2, length2); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DiffMatchPatch/DiffMatchPatch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | preview 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /DiffMatchPatch/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | internal static class Extensions 4 | { 5 | internal static void Splice(this List input, int start, int count, params T[] objects) 6 | => input.Splice(start, count, (IEnumerable)objects); 7 | 8 | /// 9 | /// replaces [count] entries starting at index [start] with the given [objects] 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | internal static void Splice(this List input, int start, int count, IEnumerable objects) 17 | { 18 | input.RemoveRange(start, count); 19 | input.InsertRange(start, objects); 20 | } 21 | 22 | internal static IEnumerable Concat(this IEnumerable items, T item) 23 | { 24 | foreach (var i in items) yield return i; 25 | yield return item; 26 | } 27 | 28 | internal static IEnumerable ItemAsEnumerable(this T item) 29 | { 30 | yield return item; 31 | } 32 | 33 | internal static IEnumerable SplitBy(this string s, char separator) 34 | { 35 | StringBuilder sb = new(); 36 | foreach (var c in s) 37 | { 38 | if (c == separator) 39 | { 40 | yield return sb.ToString(); 41 | sb.Clear(); 42 | } 43 | else 44 | { 45 | sb.Append(c); 46 | } 47 | } 48 | if (sb.Length > 0) 49 | yield return sb.ToString(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DiffMatchPatch/HalfMatchResult.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | internal readonly record struct HalfMatchResult(string Prefix1, string Suffix1, string Prefix2, string Suffix2, string CommonMiddle) 4 | { 5 | public bool IsEmpty => string.IsNullOrEmpty(CommonMiddle); 6 | 7 | public static readonly HalfMatchResult Empty = new(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty); 8 | 9 | public static bool operator >(HalfMatchResult left, HalfMatchResult right) => left.CommonMiddle.Length > right.CommonMiddle.Length; 10 | 11 | public static bool operator <(HalfMatchResult left, HalfMatchResult right) => left.CommonMiddle.Length < right.CommonMiddle.Length; 12 | public static HalfMatchResult operator -(HalfMatchResult item) => new(item.Prefix2, item.Suffix2, item.Prefix1, item.Suffix1, item.CommonMiddle); 13 | public override string ToString() => $"[{Prefix1}/{Prefix2}] - {CommonMiddle} - [{Suffix1}/{Suffix2}]"; 14 | } 15 | -------------------------------------------------------------------------------- /DiffMatchPatch/ImmutableListWithValueSemantics.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | public class ImmutableListWithValueSemantics : IImmutableList, IEquatable> 4 | { 5 | readonly ImmutableList _list; 6 | 7 | public ImmutableListWithValueSemantics(ImmutableList list) => _list = list; 8 | 9 | #region IImutableList implementation 10 | public T this[int index] => _list[index]; 11 | 12 | public int Count => _list.Count; 13 | 14 | public IImmutableList Add(T value) => _list.Add(value).WithValueSemantics(); 15 | public IImmutableList AddRange(IEnumerable items) => _list.AddRange(items).WithValueSemantics(); 16 | public IImmutableList Clear() => _list.Clear().WithValueSemantics(); 17 | public IEnumerator GetEnumerator() => _list.GetEnumerator(); 18 | public int IndexOf(T item, int index, int count, IEqualityComparer? equalityComparer) => _list.IndexOf(item, index, count, equalityComparer); 19 | IImmutableList IImmutableList.Insert(int index, T element) => _list.Insert(index, element).WithValueSemantics(); 20 | public ImmutableListWithValueSemantics Insert(int index, T element) => _list.Insert(index, element).WithValueSemantics(); 21 | public IImmutableList InsertRange(int index, IEnumerable items) => _list.InsertRange(index, items).WithValueSemantics(); 22 | public int LastIndexOf(T item, int index, int count, IEqualityComparer? equalityComparer) => _list.LastIndexOf(item, index, count, equalityComparer); 23 | public IImmutableList Remove(T value, IEqualityComparer? equalityComparer) => _list.Remove(value, equalityComparer).WithValueSemantics(); 24 | public IImmutableList RemoveAll(Predicate match) => _list.RemoveAll(match).WithValueSemantics(); 25 | IImmutableList IImmutableList.RemoveAt(int index) => _list.RemoveAt(index).WithValueSemantics(); 26 | public ImmutableListWithValueSemantics RemoveAt(int index) => _list.RemoveAt(index).WithValueSemantics(); 27 | public IImmutableList RemoveRange(IEnumerable items, IEqualityComparer? equalityComparer) => _list.RemoveRange(items, equalityComparer).WithValueSemantics(); 28 | public IImmutableList RemoveRange(int index, int count) => _list.RemoveRange(index, count).WithValueSemantics(); 29 | public IImmutableList Replace(T oldValue, T newValue, IEqualityComparer? equalityComparer) => _list.Replace(oldValue, newValue, equalityComparer).WithValueSemantics(); 30 | public IImmutableList SetItem(int index, T value) => _list.SetItem(index, value); 31 | IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); 32 | #endregion 33 | public ImmutableList.Builder ToBuilder() => _list.ToBuilder(); 34 | public bool IsEmpty => _list.IsEmpty; 35 | public override bool Equals(object obj) => Equals(obj as IImmutableList); 36 | public bool Equals(IImmutableList? other) => this.SequenceEqual(other ?? ImmutableList.Empty); 37 | public override int GetHashCode() 38 | { 39 | unchecked 40 | { 41 | return this.Aggregate(19, (h, i) => h * 19 + i?.GetHashCode() ?? 0); 42 | } 43 | } 44 | public static implicit operator ImmutableListWithValueSemantics(ImmutableList list) => list.WithValueSemantics(); 45 | public static bool operator ==(ImmutableListWithValueSemantics left, ImmutableListWithValueSemantics right) => left.Equals(right); 46 | public static bool operator !=(ImmutableListWithValueSemantics left, ImmutableListWithValueSemantics right) => !left.Equals(right); 47 | } 48 | 49 | static class Ex 50 | { 51 | public static ImmutableListWithValueSemantics WithValueSemantics(this ImmutableList list) => new(list); 52 | } 53 | -------------------------------------------------------------------------------- /DiffMatchPatch/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | namespace System.Runtime.CompilerServices; 2 | 3 | public class IsExternalInit { } 4 | -------------------------------------------------------------------------------- /DiffMatchPatch/LineToCharCompressor.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | class LineToCharCompressor 4 | { 5 | /// 6 | /// Compresses all lines of a text to a series of indexes (starting at \u0001, ending at (char)text.Length) 7 | /// 8 | /// 9 | /// 10 | /// 11 | public string Compress(ReadOnlySpan text, int maxLines = char.MaxValue) 12 | => Encode(text, maxLines); 13 | 14 | string Encode(ReadOnlySpan text, int maxLines) 15 | { 16 | var sb = new StringBuilder(); 17 | var start = 0; 18 | var end = -1; 19 | while (end < text.Length - 1) 20 | { 21 | var i = text[start..].IndexOf('\n'); 22 | end = _lineArray.Count == maxLines || i == -1 ? text.Length - 1 : i + start; 23 | var line = text[start..(end + 1)].ToString(); 24 | EnsureHashed(line); 25 | sb.Append(this[line]); 26 | start = end + 1; 27 | } 28 | return sb.ToString(); 29 | } 30 | 31 | /// 32 | /// Decompresses a series of characters that was previously compressed back to the original lines of text. 33 | /// 34 | /// 35 | /// 36 | public string Decompress(string text) 37 | => text.Aggregate(new StringBuilder(), (sb, c) => sb.Append(this[c])).Append(text.Length == char.MaxValue ? this[char.MaxValue] : "").ToString(); 38 | 39 | // e.g. _lineArray[4] == "Hello\n" 40 | // e.g. _lineHash["Hello\n"] == 4 41 | readonly List _lineArray = new(); 42 | readonly Dictionary _lineHash = new(); 43 | 44 | void EnsureHashed(string line) 45 | { 46 | if (_lineHash.ContainsKey(line)) return; 47 | _lineArray.Add(line); 48 | _lineHash.Add(line, (char)(_lineArray.Count - 1)); 49 | } 50 | 51 | char this[string line] => _lineHash[line]; 52 | string this[int c] => _lineArray[c]; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /DiffMatchPatch/MatchSettings.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | /// 4 | /// 5 | /// 6 | /// At what point is no match declared (0.0 = perfection, 1.0 = very loose). 7 | /// 8 | /// How far to search for a match (0 = exact location, 1000+ = broad match). 9 | /// A match this many characters away from the expected location will add 10 | /// 1.0 to the score (0.0 is a perfect match). 11 | /// 12 | public readonly record struct MatchSettings(float MatchThreshold, int MatchDistance) 13 | { 14 | public static MatchSettings Default { get; } = new MatchSettings(0.5f, 1000); 15 | } 16 | -------------------------------------------------------------------------------- /DiffMatchPatch/Operation.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | public enum Operation 4 | { 5 | Delete = '-', 6 | Insert = '+', 7 | Equal = ' ' 8 | } 9 | -------------------------------------------------------------------------------- /DiffMatchPatch/Patch.cs: -------------------------------------------------------------------------------- 1 | using static DiffMatchPatch.Operation; 2 | 3 | namespace DiffMatchPatch; 4 | 5 | public record Patch(int Start1, int Length1, int Start2, int Length2, ImmutableListWithValueSemantics Diffs) 6 | { 7 | public Patch Bump(int length) => this with { Start1 = Start1 + length, Start2 = Start2 + length }; 8 | 9 | public bool IsEmpty => Diffs.IsEmpty; 10 | public bool StartsWith(Operation operation) => Diffs[0].Operation == operation; 11 | public bool EndsWith(Operation operation) => Diffs[^1].Operation == operation; 12 | 13 | internal Patch AddPaddingInFront(string padding) 14 | { 15 | (var s1, var l1, var s2, var l2, var diffs) = this; 16 | 17 | var builder = diffs.ToBuilder(); 18 | (s1, l1, s2, l2) = AddPaddingInFront(builder, s1, l1, s2, l2, padding); 19 | 20 | return new Patch(s1, l1, s2, l2, builder.ToImmutable()); 21 | } 22 | 23 | internal Patch AddPaddingAtEnd(string padding) 24 | { 25 | (var s1, var l1, var s2, var l2, var diffs) = this; 26 | 27 | var builder = diffs.ToBuilder(); 28 | (s1, l1, s2, l2) = AddPaddingAtEnd(builder, s1, l1, s2, l2, padding); 29 | 30 | return new Patch(s1, l1, s2, l2, builder.ToImmutable()); 31 | } 32 | 33 | internal Patch AddPadding(string padding) 34 | { 35 | (var s1, var l1, var s2, var l2, var diffs) = this; 36 | 37 | var builder = diffs.ToBuilder(); 38 | (s1, l1, s2, l2) = AddPaddingInFront(builder, s1, l1, s2, l2, padding); 39 | (s1, l1, s2, l2) = AddPaddingAtEnd(builder, s1, l1, s2, l2, padding); 40 | 41 | return new Patch(s1, l1, s2, l2, builder.ToImmutable()); 42 | } 43 | 44 | private (int s1, int l1, int s2, int l2) AddPaddingInFront(ImmutableList.Builder builder, int s1, int l1, int s2, int l2, string padding) 45 | { 46 | if (!StartsWith(Equal)) 47 | { 48 | builder.Insert(0, Diff.Equal(padding)); 49 | return (s1 - padding.Length, l1 + padding.Length, s2 - padding.Length, l2 + padding.Length); 50 | } 51 | else if (padding.Length > Diffs[0].Text.Length) 52 | { 53 | var firstDiff = Diffs[0]; 54 | var extraLength = padding.Length - firstDiff.Text.Length; 55 | var text = padding[firstDiff.Text.Length..] + firstDiff.Text; 56 | 57 | builder.RemoveAt(0); 58 | builder.Insert(0, firstDiff.Replace(text)); 59 | return (s1 - extraLength, l1 + extraLength, s2 - extraLength, l2 + extraLength); 60 | } 61 | else 62 | { 63 | return (s1, l1, s2, l2); 64 | } 65 | 66 | } 67 | 68 | private (int s1, int l1, int s2, int l2) AddPaddingAtEnd(ImmutableList.Builder builder, int s1, int l1, int s2, int l2, string padding) 69 | { 70 | if (!EndsWith(Equal)) 71 | { 72 | builder.Add(Diff.Equal(padding)); 73 | return (s1, l1 + padding.Length, s2, l2 + padding.Length); 74 | } 75 | else if (padding.Length > Diffs[^1].Text.Length) 76 | { 77 | var lastDiff = Diffs[^1]; 78 | var extraLength = padding.Length - lastDiff.Text.Length; 79 | var text = lastDiff.Text + padding[..extraLength]; 80 | 81 | builder.RemoveAt(builder.Count - 1); 82 | builder.Add(lastDiff.Replace(text)); 83 | 84 | return (s1, l1 + extraLength, s2, l2 + extraLength); 85 | } 86 | else 87 | { 88 | return (s1, l1, s2, l2); 89 | } 90 | 91 | } 92 | 93 | /// 94 | /// Generate GNU diff's format. 95 | /// Header: @@ -382,8 +481,9 @@ 96 | /// Indicies are printed as 1-based, not 0-based. 97 | /// 98 | /// 99 | public override string ToString() 100 | { 101 | 102 | var coords1 = Length1 switch 103 | { 104 | 0 => Start1 + ",0", 105 | 1 => Convert.ToString(Start1 + 1), 106 | _ => Start1 + 1 + "," + Length1 107 | }; 108 | 109 | var coords2 = Length2 switch 110 | { 111 | 0 => Start2 + ",0", 112 | 1 => Convert.ToString(Start2 + 1), 113 | _ => Start2 + 1 + "," + Length2 114 | }; 115 | 116 | var text = new StringBuilder() 117 | .Append("@@ -") 118 | .Append(coords1) 119 | .Append(" +") 120 | .Append(coords2) 121 | .Append(" @@\n"); 122 | 123 | // Escape the body of the patch with %xx notation. 124 | foreach (var aDiff in Diffs) 125 | { 126 | text.Append((char)aDiff.Operation); 127 | text.Append(aDiff.Text.UrlEncoded()).Append("\n"); 128 | } 129 | 130 | return text.ToString(); 131 | } 132 | /// 133 | /// Compute a list of patches to turn text1 into text2. 134 | /// A set of Diffs will be computed. 135 | /// 136 | /// old text 137 | /// new text 138 | /// timeout in seconds 139 | /// Cost of an empty edit operation in terms of edit characters. 140 | /// List of Patch objects 141 | public static ImmutableListWithValueSemantics Compute(string text1, string text2, float diffTimeout = 0, short diffEditCost = 4) 142 | { 143 | using var cts = diffTimeout <= 0 144 | ? new CancellationTokenSource() 145 | : new CancellationTokenSource(TimeSpan.FromSeconds(diffTimeout)); 146 | return Compute(text1, DiffAlgorithm.Compute(text1, text2, true, true, cts.Token).CleanupSemantic().CleanupEfficiency(diffEditCost)).ToImmutableList().WithValueSemantics(); 147 | } 148 | 149 | /// 150 | /// Compute a list of patches to turn text1 into text2. 151 | /// text1 will be derived from the provided Diffs. 152 | /// 153 | /// array of diff objects for text1 to text2 154 | /// List of Patch objects 155 | public static ImmutableListWithValueSemantics FromDiffs(IEnumerable diffs) 156 | => Compute(diffs.Text1(), diffs).ToImmutableList().WithValueSemantics(); 157 | 158 | /// 159 | /// Compute a list of patches to turn text1 into text2. 160 | /// text2 is not provided, Diffs are the delta between text1 and text2. 161 | /// 162 | /// 163 | /// 164 | /// 165 | /// 166 | public static IEnumerable Compute(string text1, IEnumerable diffs, short patchMargin = 4) 167 | { 168 | if (!diffs.Any()) 169 | { 170 | yield break; // Get rid of the null case. 171 | } 172 | 173 | var charCount1 = 0; // Number of characters into the text1 string. 174 | var charCount2 = 0; // Number of characters into the text2 string. 175 | // Start with text1 (prepatch_text) and apply the Diffs until we arrive at 176 | // text2 (postpatch_text). We recreate the patches one by one to determine 177 | // context info. 178 | var prepatchText = text1; 179 | var postpatchText = text1; 180 | var newdiffs = ImmutableList.CreateBuilder(); 181 | int start1 = 0, length1 = 0, start2 = 0, length2 = 0; 182 | foreach (var aDiff in diffs) 183 | { 184 | if (!newdiffs.Any() && aDiff.Operation != Equal) 185 | { 186 | // A new patch starts here. 187 | start1 = charCount1; 188 | start2 = charCount2; 189 | } 190 | 191 | switch (aDiff.Operation) 192 | { 193 | case Insert: 194 | newdiffs.Add(aDiff); 195 | length2 += aDiff.Text.Length; 196 | postpatchText = postpatchText.Insert(charCount2, aDiff.Text); 197 | break; 198 | case Delete: 199 | length1 += aDiff.Text.Length; 200 | newdiffs.Add(aDiff); 201 | postpatchText = postpatchText.Remove(charCount2, aDiff.Text.Length); 202 | break; 203 | case Equal: 204 | if (aDiff.Text.Length <= 2 * patchMargin && newdiffs.Any() && aDiff != diffs.Last()) 205 | { 206 | // Small equality inside a patch. 207 | newdiffs.Add(aDiff); 208 | length1 += aDiff.Text.Length; 209 | length2 += aDiff.Text.Length; 210 | } 211 | 212 | if (aDiff.Text.Length >= 2 * patchMargin) 213 | { 214 | // Time for a new patch. 215 | if (newdiffs.Any()) 216 | { 217 | (start1, length1, start2, length2) = newdiffs.AddContext(prepatchText, start1, length1, start2, length2); 218 | yield return new Patch(start1, length1, start2, length2, newdiffs.ToImmutable()); 219 | start1 = start2 = length1 = length2 = 0; 220 | newdiffs.Clear(); 221 | // Unlike Unidiff, our patch lists have a rolling context. 222 | // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff 223 | // Update prepatch text & pos to reflect the application of the 224 | // just completed patch. 225 | prepatchText = postpatchText; 226 | charCount1 = charCount2; 227 | } 228 | } 229 | break; 230 | } 231 | 232 | // Update the current character count. 233 | if (aDiff.Operation != Insert) 234 | { 235 | charCount1 += aDiff.Text.Length; 236 | } 237 | if (aDiff.Operation != Delete) 238 | { 239 | charCount2 += aDiff.Text.Length; 240 | } 241 | } 242 | // Pick up the leftover patch if not empty. 243 | if (newdiffs.Any()) 244 | { 245 | (start1, length1, start2, length2) = newdiffs.AddContext(prepatchText, start1, length1, start2, length2); 246 | yield return new Patch(start1, length1, start2, length2, newdiffs.ToImmutable()); 247 | } 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /DiffMatchPatch/PatchList.cs: -------------------------------------------------------------------------------- 1 | using static DiffMatchPatch.Operation; 2 | 3 | namespace DiffMatchPatch; 4 | 5 | public static class PatchList 6 | { 7 | 8 | internal static readonly string NullPadding = new(Enumerable.Range(1, 4).Select(i => (char)i).ToArray()); 9 | 10 | /// 11 | /// Add some padding on text start and end so that edges can match something. 12 | /// Intended to be called only from within patch_apply. 13 | /// 14 | /// 15 | /// 16 | /// The padding string added to each side. 17 | internal static IEnumerable AddPadding(this IEnumerable patches, string padding) 18 | { 19 | var paddingLength = padding.Length; 20 | 21 | var enumerator = patches.GetEnumerator(); 22 | 23 | if (!enumerator.MoveNext()) 24 | yield break; 25 | 26 | Patch current = enumerator.Current.Bump(paddingLength); 27 | Patch next = current; 28 | bool isfirst = true; 29 | while (true) 30 | { 31 | var hasnext = enumerator.MoveNext(); 32 | if (hasnext) 33 | next = enumerator.Current.Bump(paddingLength); 34 | 35 | yield return (isfirst, hasnext) switch 36 | { 37 | (true, false) => current.AddPadding(padding), // list has only one patch 38 | (true, true) => current.AddPaddingInFront(padding), 39 | (false, true) => current, 40 | (false, false) => current.AddPaddingAtEnd(padding) 41 | }; 42 | 43 | isfirst = false; 44 | if (!hasnext) yield break; 45 | 46 | current = next; 47 | } 48 | } 49 | 50 | 51 | /// 52 | /// Take a list of patches and return a textual representation. 53 | /// 54 | /// 55 | /// 56 | public static string ToText(this IEnumerable patches) => patches.Aggregate(new StringBuilder(), (sb, patch) => sb.Append(patch)).ToString(); 57 | 58 | static readonly Regex PatchHeader = new("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); 59 | 60 | /// 61 | /// Parse a textual representation of patches and return a List of Patch 62 | /// objects. 63 | /// 64 | /// 65 | public static ImmutableList Parse(string text) => ParseImpl(text).ToImmutableList(); 66 | static IEnumerable ParseImpl(string text) 67 | { 68 | if (text.Length == 0) 69 | { 70 | yield break; 71 | } 72 | 73 | var lines = text.SplitBy('\n').ToArray(); 74 | var index = 0; 75 | while (index < lines.Length) 76 | { 77 | var line = lines[index]; 78 | var m = PatchHeader.Match(line); 79 | if (!m.Success) 80 | { 81 | throw new ArgumentException("Invalid patch string: " + line); 82 | } 83 | 84 | (var start1, var length1) = m.GetStartAndLength(1, 2); 85 | (var start2, var length2) = m.GetStartAndLength(3, 4); 86 | 87 | index++; 88 | 89 | IEnumerable CreateDiffs() 90 | { 91 | while (index < lines.Length) 92 | { 93 | line = lines[index]; 94 | if (!string.IsNullOrEmpty(line)) 95 | { 96 | var sign = line[0]; 97 | if (sign == '@') // Start of next patch. 98 | break; 99 | yield return sign switch 100 | { 101 | '+' => Diff.Insert(line[1..].Replace("+", "%2b").UrlDecoded()), 102 | '-' => Diff.Delete(line[1..].Replace("+", "%2b").UrlDecoded()), 103 | _ => Diff.Equal(line[1..].Replace("+", "%2b").UrlDecoded()) 104 | }; 105 | 106 | } 107 | index++; 108 | } 109 | } 110 | 111 | 112 | yield return new Patch 113 | ( 114 | start1, 115 | length1, 116 | start2, 117 | length2, 118 | CreateDiffs().ToImmutableList() 119 | ); 120 | } 121 | } 122 | 123 | 124 | static (int start, int length) GetStartAndLength(this Match m, int startIndex, int lengthIndex) 125 | { 126 | var lengthStr = m.Groups[lengthIndex].Value; 127 | var value = Convert.ToInt32(m.Groups[startIndex].Value); 128 | return lengthStr switch 129 | { 130 | "0" => (value, 0), 131 | "" => (value - 1, 1), 132 | _ => (value - 1, Convert.ToInt32(lengthStr)) 133 | }; 134 | } 135 | 136 | /// 137 | /// Merge a set of patches onto the text. Return a patched text, as well 138 | /// as an array of true/false values indicating which patches were applied. 139 | /// 140 | /// Old text 141 | /// Two element Object array, containing the new text and an array of 142 | /// bool values. 143 | 144 | public static (string newText, bool[] results) Apply(this IEnumerable patches, string text) 145 | => Apply(patches, text, MatchSettings.Default, PatchSettings.Default); 146 | 147 | 148 | public static (string newText, bool[] results) Apply(this IEnumerable patches, string text, MatchSettings matchSettings) 149 | => Apply(patches, text, matchSettings, PatchSettings.Default); 150 | 151 | /// 152 | /// Merge a set of patches onto the text. Return a patched text, as well 153 | /// as an array of true/false values indicating which patches were applied. 154 | /// 155 | /// Old text 156 | /// 157 | /// 158 | /// Two element Object array, containing the new text and an array of 159 | /// bool values. 160 | public static (string newText, bool[] results) Apply(this IEnumerable input, string text, 161 | MatchSettings matchSettings, PatchSettings settings) 162 | { 163 | if (!input.Any()) 164 | { 165 | return (text, new bool[0]); 166 | } 167 | 168 | var nullPadding = NullPadding; 169 | text = nullPadding + text + nullPadding; 170 | 171 | var patches = input.AddPadding(nullPadding).SplitMax().ToList(); 172 | 173 | var x = 0; 174 | // delta keeps track of the offset between the expected and actual 175 | // location of the previous patch. If there are patches expected at 176 | // positions 10 and 20, but the first patch was found at 12, delta is 2 177 | // and the second patch has an effective expected position of 22. 178 | var delta = 0; 179 | var results = new bool[patches.Count]; 180 | foreach (var aPatch in patches) 181 | { 182 | var expectedLoc = aPatch.Start2 + delta; 183 | var text1 = aPatch.Diffs.Text1(); 184 | int startLoc; 185 | var endLoc = -1; 186 | if (text1.Length > Constants.MatchMaxBits) 187 | { 188 | // patch_splitMax will only provide an oversized pattern 189 | // in the case of a monster delete. 190 | startLoc = text.FindBestMatchIndex(text1[..Constants.MatchMaxBits], expectedLoc, matchSettings); 191 | 192 | if (startLoc != -1) 193 | { 194 | endLoc = text.FindBestMatchIndex( 195 | text1[^Constants.MatchMaxBits..], expectedLoc + text1.Length - Constants.MatchMaxBits, matchSettings 196 | ); 197 | 198 | if (endLoc == -1 || startLoc >= endLoc) 199 | { 200 | // Can't find valid trailing context. Drop this patch. 201 | startLoc = -1; 202 | } 203 | } 204 | } 205 | else 206 | { 207 | startLoc = text.FindBestMatchIndex(text1, expectedLoc, matchSettings); 208 | } 209 | if (startLoc == -1) 210 | { 211 | // No match found. :( 212 | results[x] = false; 213 | // Subtract the delta for this failed patch from subsequent patches. 214 | delta -= aPatch.Length2 - aPatch.Length1; 215 | } 216 | else 217 | { 218 | // Found a match. :) 219 | results[x] = true; 220 | delta = startLoc - expectedLoc; 221 | int actualEndLoc; 222 | if (endLoc == -1) 223 | { 224 | actualEndLoc = Math.Min(startLoc + text1.Length, text.Length); 225 | } 226 | else 227 | { 228 | actualEndLoc = Math.Min(endLoc + Constants.MatchMaxBits, text.Length); 229 | } 230 | var text2 = text[startLoc..actualEndLoc]; 231 | if (text1 == text2) 232 | { 233 | // Perfect match, just shove the Replacement text in. 234 | text = text[..startLoc] + aPatch.Diffs.Text2() 235 | + text[(startLoc + text1.Length)..]; 236 | } 237 | else 238 | { 239 | // Imperfect match. Run a diff to get a framework of equivalent 240 | // indices. 241 | var diffs = Diff.Compute(text1, text2, 0f, false); 242 | if (text1.Length > Constants.MatchMaxBits 243 | && diffs.Levenshtein() / (float)text1.Length 244 | > settings.PatchDeleteThreshold) 245 | { 246 | // The end points match, but the content is unacceptably bad. 247 | results[x] = false; 248 | } 249 | else 250 | { 251 | diffs = diffs.CleanupSemanticLossless().ToImmutableList(); 252 | var index1 = 0; 253 | foreach (var aDiff in aPatch.Diffs) 254 | { 255 | if (aDiff.Operation != Equal) 256 | { 257 | var index2 = diffs.FindEquivalentLocation2(index1); 258 | if (aDiff.Operation == Insert) 259 | { 260 | // Insertion 261 | text = text.Insert(startLoc + index2, aDiff.Text); 262 | } 263 | else if (aDiff.Operation == Delete) 264 | { 265 | // Deletion 266 | text = text.Remove(startLoc + index2, diffs.FindEquivalentLocation2(index1 + aDiff.Text.Length) - index2); 267 | } 268 | } 269 | if (aDiff.Operation != Delete) 270 | { 271 | index1 += aDiff.Text.Length; 272 | } 273 | } 274 | } 275 | } 276 | } 277 | x++; 278 | } 279 | // Strip the padding off. 280 | text = text.Substring(nullPadding.Length, text.Length - 2 * nullPadding.Length); 281 | return (text, results); 282 | } 283 | 284 | /// 285 | /// Look through the patches and break up any which are longer than the 286 | /// maximum limit of the match algorithm. 287 | /// Intended to be called only from within patch_apply. 288 | /// 289 | /// 290 | /// 291 | internal static IEnumerable SplitMax(this IEnumerable patches, short patchMargin = 4) 292 | { 293 | var patchSize = Constants.MatchMaxBits; 294 | foreach (var patch in patches) 295 | { 296 | if (patch.Length1 <= patchSize) 297 | { 298 | yield return patch; 299 | continue; 300 | } 301 | 302 | var bigpatch = patch; 303 | // Remove the big old patch. 304 | (var start1, _, var start2, _, var diffs) = bigpatch; 305 | 306 | var precontext = string.Empty; 307 | while (diffs.Any()) 308 | { 309 | // Create one of several smaller patches. 310 | (int s1, int l1, int s2, int l2, List thediffs) 311 | = (start1 - precontext.Length, precontext.Length, start2 - precontext.Length, precontext.Length, new List()); 312 | 313 | var empty = true; 314 | 315 | if (precontext.Length != 0) 316 | { 317 | thediffs.Add(Diff.Equal(precontext)); 318 | } 319 | while (diffs.Any() && l1 < patchSize - patchMargin) 320 | { 321 | var first = diffs[0]; 322 | var diffType = diffs[0].Operation; 323 | var diffText = diffs[0].Text; 324 | 325 | if (first.Operation == Insert) 326 | { 327 | // Insertions are harmless. 328 | l2 += diffText.Length; 329 | start2 += diffText.Length; 330 | thediffs.Add(Diff.Insert(diffText)); 331 | diffs = diffs.RemoveAt(0); 332 | empty = false; 333 | } 334 | else if (first.IsLargeDelete(2 * patchSize) && thediffs.Count == 1 && thediffs[0].Operation == Equal) 335 | { 336 | // This is a large deletion. Let it pass in one chunk. 337 | l1 += diffText.Length; 338 | start1 += diffText.Length; 339 | thediffs.Add(Diff.Delete(diffText)); 340 | diffs = diffs.RemoveAt(0); 341 | empty = false; 342 | } 343 | else 344 | { 345 | // Deletion or equality. Only take as much as we can stomach. 346 | var cutoff = diffText[..Math.Min(diffText.Length, patchSize - l1 - patchMargin)]; 347 | l1 += cutoff.Length; 348 | start1 += cutoff.Length; 349 | if (diffType == Equal) 350 | { 351 | l2 += cutoff.Length; 352 | start2 += cutoff.Length; 353 | } 354 | else 355 | { 356 | empty = false; 357 | } 358 | thediffs.Add(Diff.Create(diffType, cutoff)); 359 | if (cutoff == first.Text) 360 | { 361 | diffs = diffs.RemoveAt(0); 362 | } 363 | else 364 | { 365 | diffs = diffs.RemoveAt(0).Insert(0, first with { Text = first.Text[cutoff.Length..] }); 366 | } 367 | } 368 | } 369 | 370 | // Compute the head context for the next patch. 371 | precontext = thediffs.Text2(); 372 | 373 | // if (thediffs.Text2() != precontext) throw new E 374 | precontext = precontext[Math.Max(0, precontext.Length - patchMargin)..]; 375 | 376 | // Append the end context for this patch. 377 | var text1 = diffs.Text1(); 378 | var postcontext = text1.Length > patchMargin ? text1[..patchMargin] : text1; 379 | 380 | if (postcontext.Length != 0) 381 | { 382 | l1 += postcontext.Length; 383 | l2 += postcontext.Length; 384 | var lastDiff = thediffs.Last(); 385 | if (thediffs.Count > 0 && lastDiff.Operation == Equal) 386 | thediffs[^1] = lastDiff.Append(postcontext); 387 | else 388 | thediffs.Add(Diff.Equal(postcontext)); 389 | } 390 | if (!empty) 391 | { 392 | yield return new Patch(s1, l1, s2, l2, thediffs.ToImmutableList()); 393 | } 394 | } 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /DiffMatchPatch/PatchSettings.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | /// 4 | /// When deleting a large block of text (over ~64 characters), how close 5 | /// do the contents have to be to match the expected contents. (0.0 = 6 | /// perfection, 1.0 = very loose). Note that Match_Threshold controls 7 | /// how closely the end points of a delete need to match. 8 | /// 9 | /// 10 | /// Chunk size for context length. 11 | /// 12 | public readonly record struct PatchSettings(float PatchDeleteThreshold, short PatchMargin) 13 | { 14 | public static PatchSettings Default { get; } = new PatchSettings(0.5f, 4); 15 | } 16 | -------------------------------------------------------------------------------- /DiffMatchPatch/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | 4 | [assembly: InternalsVisibleTo("DiffMatchPatch.Tests")] 5 | [assembly: InternalsVisibleTo("DiffMatchPatch.PerformanceTest")] 6 | -------------------------------------------------------------------------------- /DiffMatchPatch/Properties/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections; 3 | global using System.Collections.Generic; 4 | global using System.Linq; 5 | global using System.Collections.Immutable; 6 | global using System.Threading; 7 | global using System.Text; 8 | global using System.Text.RegularExpressions; 9 | class Usings { } -------------------------------------------------------------------------------- /DiffMatchPatch/TextUtil.cs: -------------------------------------------------------------------------------- 1 | namespace DiffMatchPatch; 2 | 3 | internal static class TextUtil 4 | { 5 | /// 6 | /// Determine the common prefix of two strings as the number of characters common to the start of each string. 7 | /// 8 | /// 9 | /// 10 | /// start index of substring in text1 11 | /// start index of substring in text2 12 | /// The number of characters common to the start of each string. 13 | internal static int CommonPrefix(ReadOnlySpan text1, ReadOnlySpan text2, int i1 = 0, int i2 = 0) 14 | { 15 | var l1 = text1.Length - i1; 16 | var l2 = text2.Length - i2; 17 | var n = Math.Min(l1, l2); 18 | for (var i = 0; i < n; i++) 19 | { 20 | if (text1[i + i1] != text2[i + i2]) 21 | { 22 | return i; 23 | } 24 | } 25 | return n; 26 | } 27 | 28 | internal static int CommonPrefix(StringBuilder text1, StringBuilder text2) 29 | { 30 | var n = Math.Min(text1.Length, text2.Length); 31 | for (var i = 0; i < n; i++) 32 | { 33 | if (text1[i] != text2[i]) 34 | { 35 | return i; 36 | } 37 | } 38 | return n; 39 | } 40 | /// 41 | /// Determine the common suffix of two strings as the number of characters common to the end of each string. 42 | /// 43 | /// 44 | /// 45 | /// maximum length to consider for text1 46 | /// maximum length to consider for text2 47 | /// The number of characters common to the end of each string. 48 | internal static int CommonSuffix(ReadOnlySpan text1, ReadOnlySpan text2, int? l1 = null, int? l2 = null) 49 | { 50 | var text1Length = l1 ?? text1.Length; 51 | var text2Length = l2 ?? text2.Length; 52 | var n = Math.Min(text1Length, text2Length); 53 | for (var i = 1; i <= n; i++) 54 | { 55 | if (text1[text1Length - i] != text2[text2Length - i]) 56 | { 57 | return i - 1; 58 | } 59 | } 60 | return n; 61 | } 62 | internal static int CommonSuffix(StringBuilder text1, StringBuilder text2) 63 | { 64 | var text1Length = text1.Length; 65 | var text2Length = text2.Length; 66 | var n = Math.Min(text1Length, text2Length); 67 | for (var i = 1; i <= n; i++) 68 | { 69 | if (text1[text1Length - i] != text2[text2Length - i]) 70 | { 71 | return i - 1; 72 | } 73 | } 74 | return n; 75 | } 76 | 77 | /// 78 | /// Determine if the suffix of one string is the prefix of another. Returns 79 | /// the number of characters common to the end of the first 80 | /// string and the start of the second string. 81 | /// 82 | /// 83 | /// 84 | /// The number of characters common to the end of the first 85 | /// string and the start of the second string. 86 | internal static int CommonOverlap(ReadOnlySpan text1, ReadOnlySpan text2) 87 | { 88 | // Cache the text lengths to prevent multiple calls. 89 | var text1Length = text1.Length; 90 | var text2Length = text2.Length; 91 | // Eliminate the null case. 92 | if (text1Length == 0 || text2Length == 0) 93 | { 94 | return 0; 95 | } 96 | // Truncate the longer string. 97 | if (text1Length > text2Length) 98 | { 99 | text1 = text1[(text1Length - text2Length)..]; 100 | } 101 | else if (text1Length < text2Length) 102 | { 103 | text2 = text2.Slice(0, text1Length); 104 | } 105 | 106 | var textLength = Math.Min(text1Length, text2Length); 107 | 108 | // look for last character of text1 in text2, from the end to the beginning 109 | // where text1 ends with the pattern from beginning of text2 until that character 110 | var last = text1[^1]; 111 | for (int length = text2.Length; length > 0; length--) 112 | { 113 | if (text2[length - 1] == last && text1.EndsWith(text2.Slice(0, length))) 114 | return length; 115 | } 116 | return 0; 117 | 118 | } 119 | 120 | /// 121 | /// Does a Substring of shorttext exist within longtext such that the 122 | /// Substring is at least half the length of longtext? 123 | /// 124 | /// Longer string. 125 | /// Shorter string. 126 | /// Start index of quarter length Substring within longtext. 127 | /// 128 | private static HalfMatchResult HalfMatchI(ReadOnlySpan longtext, ReadOnlySpan shorttext, int i) 129 | { 130 | // Start with a 1/4 length Substring at position i as a seed. 131 | var seed = longtext.Slice(i, longtext.Length / 4); 132 | var j = -1; 133 | 134 | var bestCommon = string.Empty; 135 | string bestLongtextA = string.Empty, bestLongtextB = string.Empty; 136 | string bestShorttextA = string.Empty, bestShorttextB = string.Empty; 137 | 138 | int n = j; 139 | while (n < shorttext.Length && (j = shorttext[(j + 1)..].IndexOf(seed, StringComparison.Ordinal)) != -1) 140 | { 141 | j = n = j + n + 1; 142 | var prefixLength = CommonPrefix(longtext, shorttext, i, j); 143 | var suffixLength = CommonSuffix(longtext, shorttext, i, j); 144 | if (bestCommon.Length < suffixLength + prefixLength) 145 | { 146 | bestCommon = shorttext.Slice(j - suffixLength, suffixLength).ToString() + shorttext.Slice(j, prefixLength).ToString(); 147 | bestLongtextA = longtext.Slice(0, i - suffixLength).ToString(); 148 | bestLongtextB = longtext[(i + prefixLength)..].ToString(); 149 | bestShorttextA = shorttext.Slice(0, j - suffixLength).ToString(); 150 | bestShorttextB = shorttext[(j + prefixLength)..].ToString(); 151 | } 152 | } 153 | return bestCommon.Length * 2 >= longtext.Length 154 | ? new(bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon) 155 | : HalfMatchResult.Empty; 156 | } 157 | 158 | 159 | /// 160 | /// Do the two texts share a Substring which is at least half the length of 161 | /// the longer text? 162 | /// This speedup can produce non-minimal Diffs. 163 | /// 164 | /// 165 | /// 166 | /// Data structure containing the prefix and suffix of string1, 167 | /// the prefix and suffix of string 2, and the common middle. Null if there was no match. 168 | internal static HalfMatchResult HalfMatch(ReadOnlySpan text1, ReadOnlySpan text2) 169 | { 170 | var longtext = text1.Length > text2.Length ? text1 : text2; 171 | var shorttext = text1.Length > text2.Length ? text2 : text1; 172 | if (longtext.Length < 4 || shorttext.Length * 2 < longtext.Length) 173 | { 174 | return HalfMatchResult.Empty; // Pointless. 175 | } 176 | 177 | // First check if the second quarter is the seed for a half-match. 178 | var hm1 = HalfMatchI(longtext, shorttext, (longtext.Length + 3) / 4); 179 | // Check again based on the third quarter. 180 | var hm2 = HalfMatchI(longtext, shorttext, (longtext.Length + 1) / 2); 181 | 182 | var hm = (hm1, hm2) switch 183 | { 184 | { hm1.IsEmpty: true } and { hm2.IsEmpty: true } => hm1, 185 | { hm2.IsEmpty: true } => hm1, 186 | { hm1.IsEmpty: true } => hm2, 187 | _ when hm1 > hm2 => hm1, 188 | _ => hm2 189 | }; 190 | 191 | return text1.Length > text2.Length ? hm : -hm; 192 | } 193 | private static readonly Regex HEXCODE = new("%[0-9A-F][0-9A-F]"); 194 | 195 | 196 | /// 197 | /// Encodes a string with URI-style % escaping. 198 | /// Compatible with JavaScript's encodeURI function. 199 | /// 200 | internal static string UrlEncoded(this string str) 201 | { 202 | // TODO verify if this is the right way (probably should use HttpUtility here) 203 | 204 | int MAX_LENGTH = 0xFFEF; 205 | // C# throws a System.UriFormatException if string is too long. 206 | // Split the string into 64kb chunks. 207 | StringBuilder sb = new(); 208 | int index = 0; 209 | while (index + MAX_LENGTH < str.Length) 210 | { 211 | sb.Append(Uri.EscapeDataString(str.Substring(index, MAX_LENGTH))); 212 | index += MAX_LENGTH; 213 | } 214 | sb.Append(Uri.EscapeDataString(str[index..])); 215 | // C# is overzealous in the replacements. Walk back on a few. 216 | sb = sb.Replace('+', ' ').Replace("%20", " ").Replace("%21", "!") 217 | .Replace("%2A", "*").Replace("%27", "'").Replace("%28", "(") 218 | .Replace("%29", ")").Replace("%3B", ";").Replace("%2F", "/") 219 | .Replace("%3F", "?").Replace("%3A", ":").Replace("%40", "@") 220 | .Replace("%26", "&").Replace("%3D", "=").Replace("%2B", "+") 221 | .Replace("%24", "$").Replace("%2C", ",").Replace("%23", "#"); 222 | // C# uses uppercase hex codes, JavaScript uses lowercase. 223 | 224 | return HEXCODE.Replace(sb.ToString(), s => s.Value.ToLower()); 225 | } 226 | 227 | internal static string UrlDecoded(this string str) => Uri.UnescapeDataString(str); 228 | 229 | // MATCH FUNCTIONS 230 | 231 | /// 232 | /// Locate the best instance of 'pattern' in 'text' near 'loc'. 233 | /// Returns -1 if no match found. 234 | /// 235 | /// Text to search 236 | /// pattern to search for 237 | /// location to search around 238 | /// Best match index, -1 if not found 239 | internal static int FindBestMatchIndex(this string text, string pattern, int loc) 240 | => FindBestMatchIndex(text, pattern, loc, MatchSettings.Default); 241 | 242 | internal static int FindBestMatchIndex(this string text, string pattern, int loc, MatchSettings settings) 243 | { 244 | loc = Math.Max(0, Math.Min(loc, text.Length)); 245 | 246 | 247 | 248 | if (text == pattern) 249 | { 250 | // Shortcut (potentially not guaranteed by the algorithm) 251 | return 0; 252 | } 253 | 254 | if (text.Length == 0) 255 | { 256 | // Nothing to match. 257 | return -1; 258 | } 259 | if (loc + pattern.Length <= text.Length 260 | && text.AsSpan(loc, pattern.Length).SequenceEqual(pattern)) 261 | { 262 | // Perfect match at the perfect spot! (Includes case of null pattern) 263 | return loc; 264 | } 265 | 266 | // Do a fuzzy compare. 267 | var bitap = new BitapAlgorithm(settings); 268 | return bitap.Match(text, pattern, loc); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google-diff-match-patch-csharp 2 | 3 | Evolution of the C# port of the google diff-match-patch implementation. 4 | 5 | Provides a simple object model to cope with diffs and patches. The main classes involved are `Diff` and `Patch`. Next to those, the static `DiffList` and `PatchList` classes provide some static and extension methods on `List` and `List`, respectively. 6 | 7 | ## Example usages 8 | 9 | See also the unit tests but here are some typical scenarios: 10 | 11 | var text1 = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, \r\n" + 12 | "sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna \r\n" + 13 | "sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna \r\n" + 14 | "sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna \r\n" + 15 | "aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci \r\n" + 16 | "tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. \r\n" + 17 | "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie \r\n" + 18 | "consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan\r\n" + 19 | "et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore \r\n" + 20 | "te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil \r\n" + 21 | "imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; \r\n" + 22 | "est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores \r\n" + 23 | "legere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur\r\n" + 24 | "mutationem consuetudium lectorum. Mirum est notare quam littera gothica, quam nunc putamus \r\n" + 25 | "parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta \r\n" + 26 | "decima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum."; 27 | 28 | var text2 = "Lorem ipsum dolor sit amet, adipiscing elit, \r\n" + 29 | "sed diam nonummy nibh euismod tincidunt ut laoreet dolore vobiscum magna \r\n" + 30 | "aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci \r\n" + 31 | "tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. \r\n" + 32 | "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie \r\n" + 33 | "consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan\r\n" + 34 | "et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore \r\n" + 35 | "te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil \r\n" + 36 | "imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; \r\n" + 37 | "est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores \r\n" + 38 | "legere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur\r\n" + 39 | "mutationem consuetudium lectorum. Mirum est notare quam littera gothica, putamus \r\n" + 40 | "parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta \r\n" + 41 | "decima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum."; 42 | 43 | Computing a list of diffs from 2 strings: 44 | 45 | List diffs = Diff.Compute(text1, text2); 46 | 47 | Generating a list of patches from a list of diffs: 48 | 49 | List patches = Patch.FromDiffs(diffs); 50 | 51 | Extension method to convert a list of patch objects to a textual representation: 52 | 53 | var textualRepresentation = patches.ToText(); 54 | 55 | Parse a textual representation of patches and return a List of Patch objects: 56 | 57 | List patches = PatchList.Parse(textualRepresentation); 58 | 59 | Apply a list of patches to a source text: 60 | 61 | (string newText, bool[] results) = patches.Apply(text1); 62 | Debug.Assert(results.All(result => result == true)); 63 | Debug.Assert(newText == text2); 64 | 65 | Compute the source or destination text from a list of diffs: 66 | 67 | var text1 = diffs.ToText1(); 68 | var text2 = diffs.ToText2(); 69 | 70 | Represent a list of diffs in a pretty html format: 71 | 72 | var html = diffs.PrettyHtml(); 73 | 74 | Transform a list of diffs into a string representation of the operations required to transform text1 into text2. E.g. `=4\t-1\t+ing` would transform 'skype' to 'skyping' (keep 4 chars, delete 1 char, insert 'ing'). Operations are tab-separated. Inserted text is escaped using %xx notation. 75 | 76 | var delta = diffs.ToDelta(); 77 | 78 | Given the source text and a string formatted in the 'delta' format, compute the full list of diffs: 79 | 80 | var diffs = DiffList.FromDelta(text1, delta); 81 | 82 | -------------------------------------------------------------------------------- /TestConsole/Program.cs: -------------------------------------------------------------------------------- 1 | using DiffMatchPatch; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Linq; 7 | namespace System.Runtime.CompilerServices 8 | { 9 | public class IsExternalInit { } 10 | } 11 | namespace TestConsole 12 | { 13 | class Program 14 | { 15 | static void Main(string[] args) 16 | { 17 | var diffs = new List 18 | { 19 | Diff.Insert(" "), 20 | Diff.Equal("a"), 21 | Diff.Insert("nd"), 22 | Diff.Equal(" [[Pennsylvania]]"), 23 | Diff.Delete(" and [[New") 24 | }; 25 | 26 | var patch1 = Patch.FromDiffs(diffs); 27 | 28 | Console.WriteLine(diffs.Text1()); 29 | Console.WriteLine(diffs.Text2()); 30 | 31 | var patch2 = Patch.Compute(diffs.Text1(), diffs.Text2(), 0, 4); 32 | 33 | Console.WriteLine(patch1.ToText()); 34 | Console.WriteLine(patch2.ToText()); 35 | 36 | //Debug.Assert(patch1.SequenceEqual(patch2)); 37 | 38 | ImmutableList someList = Enumerable.Range(0, 10).ToImmutableList(); 39 | var record1 = new MyRecord(someList); 40 | var record2 = new MyRecord(someList); 41 | 42 | Console.WriteLine(record1 == record2); 43 | } 44 | 45 | readonly record struct MyRecord(ImmutableList SomeList); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TestConsole/TestConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | preview 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------