using System.Collections;
using System.Collections.Generic;
public int id { get; set; }
public string BaseContent { get; set; }
public string LineNumber { get; set; }
public Editor(int id, string baseContent, string lineNumber)
BaseContent = baseContent;
public override string ToString() => $"Id {id} at Line Number {LineNumber} with Content \"{BaseContent}\"";
private static void Demo()
Editor[] original = new Editor[]
new(1, "Charterers", "1"),
new(11, "optional", "11"),
Editor[] final = new Editor[]
new(17, "Charterers", "5"),
new(25, "together", "13"),
new(28, "optional", "16"),
var edit = original.ToEditProcedure(final,
(a, b) => a.BaseContent != b.BaseContent ? 1
: a.LineNumber != b.LineNumber ? 0.5
foreach (var op in edit.EditSequence)
if (op.Kind == EditOperationKind.None)
if (op.Kind == EditOperationKind.Insert)
Console.WriteLine($"Inserted: {op.After.ToString()}");
else if (op.Kind == EditOperationKind.Delete)
Console.WriteLine($"Deleted: {op.Before.ToString()}");
else if (op.Kind == EditOperationKind.Edit && op.Cost > 0)
string textBefore = op.Before.BaseContent;
string textAfter = op.After.BaseContent;
var editDetails = textBefore.ToEditProcedure(textAfter);
if (editDetails.EditDistance > 0)
Console.WriteLine($"Edited: {textBefore} -> {textAfter} (id {op.Before.id} to id {op.After.id})\r\n" + editDetails.ToString());
Console.WriteLine($"Edited: {textBefore} -> {textAfter} (id {op.Before.id} to id {op.After.id}) only LineNumber should be changed: {op.Before.LineNumber} to {op.After.LineNumber}");
public static void Main()
public enum EditOperationKind
public sealed class EditOperation<T>
: IEquatable<EditOperation<T>>
internal string ToReport()
EditOperationKind.None => $"Keep '{Before}', ({Cost})",
EditOperationKind.Insert => $"Insert '{After}', ({Cost})",
EditOperationKind.Delete => $"Delete '{Before}', ({Cost})",
EditOperationKind.Edit => $"Edit '{Before}' into '{After}', ({Cost})",
_ => $"??? '{Before}' into '{After}', ({Cost})",
internal EditOperation(EditOperationKind kind, T before, T after, double cost)
public EditOperationKind Kind { get; }
public double Cost { get; }
public override string ToString()
EditOperationKind.None => $"Keep '{Before}'",
EditOperationKind.Insert => $"Insert '{After}'",
EditOperationKind.Delete => $"Delete '{Before}'",
EditOperationKind.Edit => $"Edit '{Before}' into '{After}'",
_ => $"Unknown '{Before}' into '{After}'",
public static bool operator ==(EditOperation<T> left, EditOperation<T> right)
if (ReferenceEquals(left, right))
else if (left is null || right is null)
return left.Equals(right);
public static bool operator !=(EditOperation<T> left, EditOperation<T> right)
if (ReferenceEquals(left, right))
else if (left is null || right is null)
return !left.Equals(right);
#region IEquatable<EditOperation<T>
public bool Equals(EditOperation<T> other)
if (ReferenceEquals(this, other))
return Equals(Before, other.Before) &&
Equals(After, other.After) &&
public override bool Equals(object obj) =>
obj is EditOperation<T> other && Equals(other);
public override int GetHashCode()
return (Before is null ? 0 : Before.GetHashCode()) ^
(After is null ? 0 : After.GetHashCode()) ^
#endregion IEquatable<EditOperation<T>
public interface IEditCost<T>
double InsertionPrice(T value);
double DeletionPrice(T value);
double EditPrice(T from, T to);
public static class EditCosts<T>
private class UniformEditCost : IEditCost<T>
public double DeletionPrice(T value) => 1.0;
public double EditPrice(T from, T to) => Equals(from, to) ? 0 : 1.0;
public double InsertionPrice(T value) => 1.0;
private class InsertDeleteEditCost : IEditCost<T>
public double DeletionPrice(T value) => 1.0;
public double EditPrice(T from, T to) => Equals(from, to) ? 0 : 2.0;
public double InsertionPrice(T value) => 1.0;
public static IEditCost<T> Uniform
} = new UniformEditCost();
public static IEditCost<T> Standard
} = new InsertDeleteEditCost();
public sealed class EditProcedure<T> : IReadOnlyList<EditOperation<T>>
private List<EditOperation<T>> m_Sequence;
private void CorePerform(T[] source,
Func<T, double> insertCost,
Func<T, double> deleteCost,
Func<T, T, double> editCost)
EditOperationKind[][] M = Enumerable
.Range(0, source.Length + 1)
.Select(line => new EditOperationKind[target.Length + 1])
double[][] D = Enumerable
.Range(0, source.Length + 1)
.Select(line => new double[target.Length + 1])
for (int i = 1; i <= source.Length; ++i)
M[i][0] = EditOperationKind.Delete;
D[i][0] = (sum += deleteCost(source[i - 1]));
for (int i = 1; i <= target.Length; ++i)
M[0][i] = EditOperationKind.Insert;
D[0][i] = (sum += insertCost(target[i - 1]));
for (int i = 1; i <= source.Length; ++i)
for (int j = 1; j <= target.Length; ++j)
double insert = D[i][j - 1] + insertCost(target[j - 1]);
double delete = D[i - 1][j] + deleteCost(source[i - 1]);
double edit = D[i - 1][j - 1] + editCost(source[i - 1], target[j - 1]);
double min = Math.Min(Math.Min(insert, delete), edit);
M[i][j] = EditOperationKind.Insert;
M[i][j] = EditOperationKind.Delete;
M[i][j] = object.Equals(source[i - 1], target[j - 1])
: EditOperationKind.Edit;
EditDistance = D[source.Length][target.Length];
new List<EditOperation<T>>(source.Length + target.Length);
for (int x = target.Length, y = source.Length; (x > 0) || (y > 0);)
EditOperationKind op = M[y][x];
if (op == EditOperationKind.Insert)
m_Sequence.Add(new EditOperation<T>(op, default, target[x], D[y][x + 1] - D[y][x]));
else if (op == EditOperationKind.Delete)
m_Sequence.Add(new EditOperation<T>(op, source[y], default, D[y + 1][x] - D[y][x]));
else if (op == EditOperationKind.Edit || op == EditOperationKind.None)
m_Sequence.Add(new EditOperation<T>(op, source[y], target[x], D[y + 1][x + 1] - D[y][x]));
public EditProcedure(IEnumerable<T> source,
Func<T, double> insertCost,
Func<T, double> deleteCost,
Func<T, T, double> editCost)
throw new ArgumentNullException(nameof(source));
throw new ArgumentNullException(nameof(target));
else if (insertCost is null)
throw new ArgumentNullException(nameof(insertCost));
else if (deleteCost is null)
throw new ArgumentNullException(nameof(deleteCost));
else if (editCost is null)
throw new ArgumentNullException(nameof(editCost));
CorePerform(source.ToArray(),
public EditProcedure(IEnumerable<T> source,
throw new ArgumentNullException(nameof(source));
throw new ArgumentNullException(nameof(target));
throw new ArgumentNullException(nameof(price));
Func<T, double> insertCost = price.InsertionPrice;
Func<T, double> deleteCost = price.DeletionPrice;
Func<T, T, double> editCost = price.EditPrice;
CorePerform(source.ToArray(),
public double EditDistance
public IReadOnlyList<EditOperation<T>> EditSequence => m_Sequence;
public override string ToString()
return string.Join(Environment.NewLine,
$"Distance: {EditDistance}",
$"Sequence ({m_Sequence.Count} steps):",
string.Join(Environment.NewLine, m_Sequence
.Select(item => $" {item.ToReport()}")));
#region IReadOnlyList<EditOperation<T>>
public int Count => m_Sequence.Count;
public EditOperation<T> this[int index] => m_Sequence[index];
public IEnumerator<EditOperation<T>> GetEnumerator() => m_Sequence.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => m_Sequence.GetEnumerator();
#endregion IReadOnlyList<EditOperation<T>>
public static partial class EnumerableExtensions
public static EditProcedure<T> ToEditProcedure<T>(this IEnumerable<T> source,
Func<T, double> insertCost,
Func<T, double> deleteCost,
Func<T, T, double> editCost)
return new EditProcedure<T>(source, target, insertCost, deleteCost, editCost);
public static EditProcedure<T> ToEditProcedure<T>(this IEnumerable<T> source,
return new EditProcedure<T>(source,
(x, y) => object.Equals(x, y) ? 0 : editCost);
public static EditProcedure<T> ToEditProcedure<T>(this IEnumerable<T> source,
return new EditProcedure<T>(source, target, prices);