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
52 changes: 52 additions & 0 deletions 2024/Advent.Tests/Commands/Day23CommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Advent.Common.Settings;
using Spectre.Console.Cli;

namespace Advent.Tests.Commands;

public class Day23CommandTests
{
private readonly List<string> _arguments = [];
private readonly IRemainingArguments _remaining = Substitute.For<IRemainingArguments>();
private readonly TestConsole console = new();

public Day23CommandTests()
{
console.Profile.Capabilities.Interactive = true;
}

[Fact]
public async Task Day23Command_Solves_Part1_Correctly()
{
var mockReader = Substitute.For<IFileReader>();
mockReader
.ReadInputAsync(Arg.Any<string>())
.Returns(Task.FromResult(TestData.Day23TestData));

var command = new Day23Command(mockReader, console);
var result = await command.ExecuteAsync(
new CommandContext(_arguments, _remaining, "day23", null),
new AdventSettings { Part = "Part 1" }
);
result.Should().Be(0);
console.Output.Should().Contain("Day 23 Part 1");
console.Output.Should().Contain("The answer is 7");
}

[Fact]
public async Task Day23Command_Solves_Part2_Correctly()
{
var mockReader = Substitute.For<IFileReader>();
mockReader
.ReadInputAsync(Arg.Any<string>())
.Returns(Task.FromResult(TestData.Day23TestData));

var command = new Day23Command(mockReader, console);
var result = await command.ExecuteAsync(
new CommandContext(_arguments, _remaining, "day23", null),
new AdventSettings { Part = "Part 2" }
);
result.Should().Be(0);
console.Output.Should().Contain("Day 23 Part 2");
console.Output.Should().Contain("The answer is co,de,ka,ta");
}
}
34 changes: 34 additions & 0 deletions 2024/Advent.Tests/Common/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,38 @@ internal static class TestData

public const string Day21TestData = "029A\n" + "980A\n" + "179A\n" + "456A\n" + "379A\n";
public const string Day22TestData = "1\n" + "10\n" + "100\n" + "2024\n";

public const string Day23TestData =
"kh-tc\n"
+ "qp-kh\n"
+ "de-cg\n"
+ "ka-co\n"
+ "yn-aq\n"
+ "qp-ub\n"
+ "cg-tb\n"
+ "vc-aq\n"
+ "tb-ka\n"
+ "wh-tc\n"
+ "yn-cg\n"
+ "kh-ub\n"
+ "ta-co\n"
+ "de-co\n"
+ "tc-td\n"
+ "tb-wq\n"
+ "wh-td\n"
+ "ta-ka\n"
+ "td-qp\n"
+ "aq-cg\n"
+ "wq-ub\n"
+ "ub-vc\n"
+ "de-ta\n"
+ "wq-aq\n"
+ "wq-vc\n"
+ "wh-yn\n"
+ "ka-de\n"
+ "kh-ta\n"
+ "co-tc\n"
+ "wh-qp\n"
+ "tb-vc\n"
+ "td-yn\n";
}
48 changes: 48 additions & 0 deletions 2024/Advent.Tests/IntegrationTests/CommandAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,54 @@ public async Task Day22Part2_IntegrationTest_Success()
result.Output.Should().Contain("The answer is 1570");
}
#endregion

#region Day23
[Fact]
public async Task Day23Part1_IntegrationTest_Success()
{
// Arrange
var args = new string[] { "day23", "--part", "Part 1" };
var app = new CommandAppTester(_registrar);

app.Configure(config =>
{
config.PropagateExceptions();
config.ConfigureConsole(_console);
config.AddCommand<Day23Command>("day23");
});

// Act
var result = await app.RunAsync(args);

// Assert
result.ExitCode.Should().Be(0);
result.Output.Should().Contain("Day 23 Part 1");
result.Output.Should().Contain("The answer is 1227");
}

[Fact]
public async Task Day23Part2_IntegrationTest_Success()
{
// Arrange
var args = new string[] { "day23", "--part", "Part 2" };
var app = new CommandAppTester(_registrar);

app.Configure(config =>
{
config.PropagateExceptions();
config.ConfigureConsole(_console);
config.AddCommand<Day23Command>("day23");
});

// Act
var result = await app.RunAsync(args);

// Assert
result.ExitCode.Should().Be(0);
result.Output.Should().Contain("Day 23 Part 2");
result.Output.Should().Contain("The answer is cl,df,ft,ir,iy,ny,qp,rb,sh,sl,sw,wm,wy");
}
#endregion
}

public class TestFixture
Expand Down
53 changes: 53 additions & 0 deletions 2024/Advent.Tests/UseCases/Day23/Day23ParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace Advent.Tests.UseCases.Day23;

using Advent.UseCases.Day23;

public class Day23ParserTests
{
[Fact]
public void Parse_WhenInputIsValid_ReturnsParsedData()
{
// Arrange
var expected = new List<(string, string)>
{
("kh", "tc"),
("qp", "kh"),
("de", "cg"),
("ka", "co"),
("yn", "aq"),
("qp", "ub"),
("cg", "tb"),
("vc", "aq"),
("tb", "ka"),
("wh", "tc"),
("yn", "cg"),
("kh", "ub"),
("ta", "co"),
("de", "co"),
("tc", "td"),
("tb", "wq"),
("wh", "td"),
("ta", "ka"),
("td", "qp"),
("aq", "cg"),
("wq", "ub"),
("ub", "vc"),
("de", "ta"),
("wq", "aq"),
("wq", "vc"),
("wh", "yn"),
("ka", "de"),
("kh", "ta"),
("co", "tc"),
("wh", "qp"),
("tb", "vc"),
("td", "yn"),
};

// Act
var actual = Day23Parser.Parse(TestData.Day23TestData);

// Assert
actual.Should().BeEquivalentTo(expected);
}
}
21 changes: 21 additions & 0 deletions 2024/Advent.Tests/UseCases/Day23/Day23Part1SolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Advent.UseCases.Day23;

namespace Advent.Tests.UseCases.Day23;

public class Day23Part1SolverTests
{
[Fact]
public void Solve_WhenInputIsValid_ReturnsExpectedResult()
{
// Arrange
var input = Day23Parser.Parse(TestData.Day23TestData);
var expected = "7";
var solver = new Day23Part1Solver();

// Act
var actual = solver.Solve(input);

// Assert
actual.Should().Be(expected);
}
}
21 changes: 21 additions & 0 deletions 2024/Advent.Tests/UseCases/Day23/Day23Part2SolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Advent.UseCases.Day23;

namespace Advent.Tests.UseCases.Day23;

public class Day23Part2SolverTests
{
[Fact]
public void Solve_WhenInputIsValid_ReturnsExpectedResult()
{
// Arrange
var input = Day23Parser.Parse(TestData.Day23TestData);
var expected = "co,de,ka,ta";
var solver = new Day23Part2Solver();

// Act
var actual = solver.Solve(input);

// Assert
actual.Should().Be(expected);
}
}
31 changes: 31 additions & 0 deletions 2024/Advent/Commands/Day23Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Advent.Common;
using Advent.Common.Commands;
using Advent.Common.Settings;
using Advent.UseCases.Day23;
using Spectre.Console;
using Spectre.Console.Cli;

namespace Advent.Commands;

public class Day23Command(IFileReader reader, IAnsiConsole console)
: AdventCommand<AdventSettings>(reader, console)
{
public override async Task<int> ExecuteAsync(CommandContext context, AdventSettings settings)
{
var input = await _reader.ReadInputAsync("../input/day23input.txt");
var data = Day23Parser.Parse(input);

var choice = settings.Part ?? PromptForPartChoice();
IDay23Solver solver = choice switch
{
"Part 1" => new Day23Part1Solver(),
"Part 2" => new Day23Part2Solver(),
_ => throw new InvalidOperationException("Invalid choice")
};

var result = solver.Solve(data);
_console.MarkupLine($"[bold green]Day 23 {choice} [/]");
_console.MarkupLine($"The answer is [bold yellow]{result}[/]");
return 0;
}
}
1 change: 1 addition & 0 deletions 2024/Advent/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
config.AddCommand<Day20Command>("day20").WithDescription("Advent of Code 2024 Day 20");
config.AddCommand<Day21Command>("day21").WithDescription("Advent of Code 2024 Day 21");
config.AddCommand<Day22Command>("day22").WithDescription("Advent of Code 2024 Day 22");
config.AddCommand<Day23Command>("day23").WithDescription("Advent of Code 2024 Day 23");
});

return await app.RunAsync(args);
66 changes: 66 additions & 0 deletions 2024/Advent/UseCases/Day23/Day23Helper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace Advent.UseCases.Day23;

internal static class Day23Helper
{
public static void PopulateGraphFromInput(
List<(string, string)> edges,
Dictionary<string, HashSet<string>> adjacencyList
)
{
void AddEdge(string from, string to)
{
if (!adjacencyList.TryGetValue(from, out var neighbors))
{
neighbors = [];
adjacencyList[from] = neighbors;
}
neighbors.Add(to);
}

foreach (var (node1, node2) in edges)
{
AddEdge(node1, node2);
AddEdge(node2, node1);
}
}

public static void ProcessGraph(
Dictionary<string, HashSet<string>> graph,
List<List<string>> cliques
)
{
BronKerboschForAllCliques([], [.. graph.Keys], [], graph, cliques);
}

private static void BronKerboschForAllCliques(
List<string> R,
List<string> P,
List<string> X,
Dictionary<string, HashSet<string>> graph,
List<List<string>> cliques
)
{
// Store every intermediate clique (not just maximal)
if (R.Count > 0)
{
cliques.Add([.. R]);
}

// Make a copy of P to avoid modifying while iterating
foreach (var node in P.ToList())
{
var neighbors = graph[node];

BronKerboschForAllCliques(
[.. R, node],
P.Intersect(neighbors).ToList(),
X.Intersect(neighbors).ToList(),
graph,
cliques
);

P.Remove(node);
X.Add(node);
}
}
}
13 changes: 13 additions & 0 deletions 2024/Advent/UseCases/Day23/Day23Parser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Advent.UseCases.Day23;

public static class Day23Parser
{
public static List<(string, string)> Parse(string input)
{
return input
.Split("\n", StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Split("-"))
.Select(parts => (parts[0], parts[1]))
.ToList();
}
}
24 changes: 24 additions & 0 deletions 2024/Advent/UseCases/Day23/Day23Part1Solver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Diagnostics;
using Spectre.Console;

namespace Advent.UseCases.Day23;

internal class Day23Part1Solver : IDay23Solver
{
public readonly Dictionary<string, HashSet<string>> _graph = [];
public readonly List<List<string>> _cliques = [];

public string Solve(List<(string, string)> input)
{
Stopwatch sw = new();
sw.Start();
Day23Helper.PopulateGraphFromInput(input, _graph);
Day23Helper.ProcessGraph(_graph, _cliques);
sw.Stop();
var count = _cliques.Count(clique =>
clique.Count == 3 && clique.Any(node => node.StartsWith('t'))
);
AnsiConsole.WriteLine($"Elapsed time: {sw.Elapsed.TotalMilliseconds} ms");
return count.ToString();
}
}
Loading
Loading