Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>

<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../BurrowsWheelerTransform/BurrowsWheelerTransform.csproj"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This is tests project")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace BurrowsWheelerTransform.Tests;

public class TransformResultTests
{
[Test]
public void TransformResultConstructor_ShouldNotThrow()
{
Assert.DoesNotThrow(() => new TransformResult(string.Empty, -1));
Assert.DoesNotThrow(() => new TransformResult("A", 0));
Assert.DoesNotThrow(() => new TransformResult("ABCDEF", 3));
Assert.DoesNotThrow(() => new TransformResult("QWERTY", 5));
}

[Test]
public void TransformResultConstructor_ShouldThrow()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new TransformResult(string.Empty, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new TransformResult("ABCD", -1));
Assert.Throws<ArgumentOutOfRangeException>(() => new TransformResult("ABCD", 4));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace BurrowsWheelerTransform.Tests;

public class TransformTests
{
private static readonly List<string> TestData =
[
string.Empty,

"A",
"BB",
"CCCCCC",
"ABACABA",
"ABABABABAB",

..GetRandomStrings()
];

[Test]
public void InverseTransform_ShouldBe_SameAs_Input([ValueSource(nameof(TestData))] string input)
{
var result = Transform.ForwardTransform(input);
var reconstructed = Transform.InverseTransform(result);
Assert.That(input.SequenceEqual(reconstructed), Is.True);
}

private static IEnumerable<string> GetRandomStrings()
{
int seed = 1743658243;
var random = new Random(seed);

int steps = 16;
int length = 256;
var buffer = new char[length];

for (int i = 0; i < steps; i++)
{
for (int j = 0; j < length; j++)
{
buffer[j] = (char)random.Next(' ', '~' + 1);
}

yield return new(buffer);
}
}
}
48 changes: 48 additions & 0 deletions BurrowsWheelerTransform/BurrowsWheelerTransform.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BurrowsWheelerTransform", "BurrowsWheelerTransform\BurrowsWheelerTransform.csproj", "{1DA138D9-04B6-498B-9C33-05E9684F1D5C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BurrowsWheelerTransform.Tests", "BurrowsWheelerTransform.Tests\BurrowsWheelerTransform.Tests.csproj", "{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Debug|x64.ActiveCfg = Debug|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Debug|x64.Build.0 = Debug|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Debug|x86.ActiveCfg = Debug|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Debug|x86.Build.0 = Debug|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Release|Any CPU.Build.0 = Release|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Release|x64.ActiveCfg = Release|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Release|x64.Build.0 = Release|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Release|x86.ActiveCfg = Release|Any CPU
{1DA138D9-04B6-498B-9C33-05E9684F1D5C}.Release|x86.Build.0 = Release|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Debug|x64.ActiveCfg = Debug|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Debug|x64.Build.0 = Debug|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Debug|x86.ActiveCfg = Debug|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Debug|x86.Build.0 = Debug|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Release|Any CPU.Build.0 = Release|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Release|x64.ActiveCfg = Release|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Release|x64.Build.0 = Release|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Release|x86.ActiveCfg = Release|Any CPU
{FDF7CD5C-DC17-4A89-B7B8-12B1A75EF86D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
123 changes: 123 additions & 0 deletions BurrowsWheelerTransform/BurrowsWheelerTransform/Transform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
namespace BurrowsWheelerTransform;

using System.Diagnostics;

/// <summary>
/// Burrows-Wheeler transform implementation.
/// </summary>
public static class Transform
{
/// <summary>
/// Transforms given string using Burrows-Wheeler algorithm.
/// </summary>
/// <param name="input">Input string.</param>
/// <returns>Result of transformation.</returns>
public static TransformResult ForwardTransform(string input)
{
int length = input.Length;

if (length == 0)
{
return new(string.Empty, -1);
}

Span<int> offsets = stackalloc int[length];
for (int i = 0; i < length; i++)
{
offsets[i] = i;
}

int Compare(int x, int y)
{
for (int i = 0; i < length; i++)
{
int compare = input[(i + x) % length] - input[(i + y) % length];
if (compare != 0)
{
return compare;
}
}

return 0;
}

offsets.Sort(Compare);

int? identityPosition = null;
Span<char> result = stackalloc char[length];
for (int i = 0; i < length; i++)
{
if (offsets[i] == 0)
{
identityPosition = i;
}

result[i] = input[(offsets[i] + length - 1) % length];
}

Debug.Assert(identityPosition.HasValue, "Identity position not found");

return new(new(result), identityPosition.Value);
}

/// <summary>
/// Reconstructs string transformed with Burrows-Wheeler algorithm.
/// </summary>
/// <param name="result">Transformed string.</param>
/// <returns>Reconstructed string.</returns>
public static string InverseTransform(TransformResult result)
{
if (result.IdentityIndex == -1)
{
return string.Empty;
}

int length = result.Value.Length;

Span<int> appearances = stackalloc int[length];
var lastAppearances = new Dictionary<char, int>();
var charCounter = new SortedDictionary<char, int>();

for (int i = 0; i < length; i++)
{
char currentChar = result.Value[i];

if (!charCounter.TryGetValue(currentChar, out int count))
{
charCounter[currentChar] = 0;
}

charCounter[currentChar]++;

appearances[i] =
lastAppearances.TryGetValue(currentChar, out int lastIndex)
? appearances[lastIndex] + 1
: 0;

lastAppearances[currentChar] = i;
}

var lesserCharsCounter = new Dictionary<char, int>();
int previousCount = 0;
foreach (var (character, count) in charCounter)
{
lesserCharsCounter[character] = previousCount;
previousCount += count;
}

Span<char> reconstructed = stackalloc char[length];

int lastIdentityIndex = result.IdentityIndex;
char lastCharacter = result.Value[lastIdentityIndex];
reconstructed[^1] = result.Value[result.IdentityIndex];

for (int i = 1; i < length; i++)
{
lastIdentityIndex = appearances[lastIdentityIndex] + lesserCharsCounter[lastCharacter];
lastCharacter = result.Value[lastIdentityIndex];
reconstructed[^(i + 1)] = lastCharacter;
}

return new(reconstructed);
}
}
45 changes: 45 additions & 0 deletions BurrowsWheelerTransform/BurrowsWheelerTransform/TransformResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace BurrowsWheelerTransform;

/// <summary>
/// Result of Burrows-Wheeler Transform.
/// </summary>
public readonly struct TransformResult
{
/// <summary>
/// Initializes a new instance of the <see cref="TransformResult"/> struct.
/// </summary>
/// <param name="value">Transformed string.</param>
/// <param name="identityIndex">Index that is used to reconstruct string.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="identityIndex"/> is out of range (less than 0 or greater or equal to <paramref name="identityIndex"/>.Length)
/// or not equal to -1 if <paramref name="identityIndex"/> is <see cref="string.Empty"/>.
/// </exception>
public TransformResult(string value, int identityIndex)
{
if (value.Length == 0)
{
if (identityIndex != -1)
{
throw new ArgumentOutOfRangeException(nameof(identityIndex), "Identity index of an empty string must be equal to -1");
}
}
else
{
ArgumentOutOfRangeException.ThrowIfNegative(identityIndex, nameof(identityIndex));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(identityIndex, value.Length, nameof(identityIndex));
}

Value = value;
IdentityIndex = identityIndex;
}

/// <summary>
/// Gets transformed string.
/// </summary>
public string Value { get; }

/// <summary>
/// Gets identity index.
/// </summary>
public int IdentityIndex { get; }
}