├── .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 | "a¶
<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