From 7f4634d09aba5876d07d34889a1c8b438ec9ba81 Mon Sep 17 00:00:00 2001 From: Moley-Bot Date: Thu, 15 Jan 2026 10:23:33 +0000 Subject: [PATCH 1/4] Add collector for EastDevonDistrictCouncil Closes #24 Generated with Codex CLI by Moley-Bot --- .../Councils/EastDevonDistrictCouncil.cs | 228 ++++++++++++++++++ .../Councils/EastDevonDistrictCouncilTests.cs | 36 +++ 2 files changed, 264 insertions(+) create mode 100644 BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs create mode 100644 BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs diff --git a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs new file mode 100644 index 0000000..f608a3b --- /dev/null +++ b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs @@ -0,0 +1,228 @@ +namespace BinDays.Api.Collectors.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors.Vendors; +using BinDays.Api.Collectors.Models; +using BinDays.Api.Collectors.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Text.Json; +using System.Text.RegularExpressions; + +/// +/// Collector implementation for East Devon District Council. +/// +internal sealed partial class EastDevonDistrictCouncil : GovUkCollectorBase, ICollector +{ + /// + public string Name => "East Devon District Council"; + + /// + public Uri WebsiteUrl => new("https://eastdevon.gov.uk/"); + + /// + public override string GovUkId => "east-devon"; + + /// + /// The list of bin types for this collector. + /// + private readonly IReadOnlyCollection _binTypes = + [ + new() + { + Name = "General Waste", + Colour = BinColour.Black, + Keys = [ "Rubbish" ], + }, + new() + { + Name = "Recycling (Paper/Glass/Cardboard)", + Colour = BinColour.Green, + Keys = [ "Recycling and food waste" ], + Type = BinType.Box, + }, + new() + { + Name = "Recycling (Plastics/Tins)", + Colour = BinColour.Green, + Keys = [ "Recycling and food waste" ], + Type = BinType.Sack, + }, + new() + { + Name = "Food Waste", + Colour = BinColour.Blue, + Keys = [ "Recycling and food waste" ], + Type = BinType.Caddy, + }, + new() + { + Name = "Garden Waste", + Colour = BinColour.Green, + Keys = [ "Green waste" ], + }, + ]; + + /// + /// Regex for parsing month headers and collection entries from the calendar. + /// + [GeneratedRegex(@"
  • ]*>

    (?[A-Za-z]+\s+\d{4})

  • |
  • (?[^<]+)(?.*?)
  • ", RegexOptions.IgnoreCase | RegexOptions.Singleline)] + private static partial Regex CollectionEntryRegex(); + + /// + /// Regex for extracting bin names from a collection entry. + /// + [GeneratedRegex(@"(?[^<]+)", RegexOptions.IgnoreCase)] + private static partial Regex BinNameRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting addresses + if (clientSideResponse == null) + { + var encodedPostcode = WebUtility.UrlEncode(postcode); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = $"https://eastdevon.gov.uk/addressfinder?qtype=bins&term={encodedPostcode}", + Method = "GET", + Headers = new() { + {"User-Agent", Constants.UserAgent}, + {"X-Requested-With", "XMLHttpRequest"}, + }, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest + }; + + return getAddressesResponse; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 1) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var addresses = new List
    (); + + // Iterate through each address, and create a new address object + foreach (var element in jsonDoc.RootElement.EnumerateArray()) + { + var property = element.GetProperty("label").GetString()!.Trim(); + var uprn = element.GetProperty("UPRN").GetString()!.Trim(); + + if (string.IsNullOrWhiteSpace(uprn)) + { + continue; + } + + var address = new Address + { + Property = property, + Postcode = postcode, + Uid = uprn, + }; + + addresses.Add(address); + } + + var getAddressesResponse = new GetAddressesResponse + { + Addresses = [.. addresses], + }; + + return getAddressesResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } + + /// + public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting bin days + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = $"https://eastdevon.gov.uk/recycling-and-waste/recycling-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN={address.Uid}", + Method = "GET", + Headers = new() { + {"User-Agent", Constants.UserAgent}, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest + }; + + return getBinDaysResponse; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 1) + { + var content = clientSideResponse.Content; + + var currentMonth = string.Empty; + var binDays = new List(); + + // Iterate through each calendar entry, and build bin day objects + foreach (Match collectionEntry in CollectionEntryRegex().Matches(content)!) + { + var month = collectionEntry.Groups["month"].Value; + if (!string.IsNullOrWhiteSpace(month)) + { + currentMonth = WebUtility.HtmlDecode(month).Trim(); + continue; + } + + var dateText = WebUtility.HtmlDecode(collectionEntry.Groups["date"].Value).Trim(); + var day = dateText.Split(" ", StringSplitOptions.RemoveEmptyEntries)[0]; + + var date = DateOnly.ParseExact( + $"{day} {currentMonth}", + "d MMMM yyyy", + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + var binsHtml = WebUtility.HtmlDecode(collectionEntry.Groups["bins"].Value); + var bins = new List(); + + // Iterate through each bin, and map to configured bin types + foreach (Match binMatch in BinNameRegex().Matches(binsHtml)!) + { + var binName = binMatch.Groups["bin"].Value.Trim(); + var matchedBins = ProcessingUtilities.GetMatchingBins(_binTypes, binName); + + bins.AddRange(matchedBins); + } + + var binDay = new BinDay + { + Date = date, + Address = address, + Bins = [.. bins], + }; + + binDays.Add(binDay); + } + + var getBinDaysResponse = new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(binDays), + }; + + return getBinDaysResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } +} diff --git a/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs new file mode 100644 index 0000000..6595b25 --- /dev/null +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs @@ -0,0 +1,36 @@ +namespace BinDays.Api.IntegrationTests.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors; +using BinDays.Api.Collectors.Collectors.Councils; +using BinDays.Api.Collectors.Services; +using BinDays.Api.IntegrationTests.Helpers; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +public class EastDevonDistrictCouncilTests +{ + private readonly IntegrationTestClient _client; + private static readonly ICollector _collector = new EastDevonDistrictCouncil(); + private readonly CollectorService _collectorService = new([_collector]); + private readonly ITestOutputHelper _outputHelper; + + public EastDevonDistrictCouncilTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _client = new IntegrationTestClient(outputHelper); + } + + [Theory] + [InlineData("EX10 8UB")] + public async Task GetBinDaysTest(string postcode) + { + await TestSteps.EndToEnd( + _client, + _collectorService, + _collector, + postcode, + _outputHelper + ); + } +} From 49da6c34e8382574a435006efe9dbccd20111413 Mon Sep 17 00:00:00 2001 From: Moley-Bot Date: Thu, 15 Jan 2026 10:24:15 +0000 Subject: [PATCH 2/4] Auto-format code with dotnet format Formatted by Moley-Bot --- .../Councils/EastDevonDistrictCouncil.cs | 456 +++++++++--------- .../Councils/EastDevonDistrictCouncilTests.cs | 72 +-- 2 files changed, 264 insertions(+), 264 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs index f608a3b..68533bd 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs @@ -1,228 +1,228 @@ -namespace BinDays.Api.Collectors.Collectors.Councils; - -using BinDays.Api.Collectors.Collectors.Vendors; -using BinDays.Api.Collectors.Models; -using BinDays.Api.Collectors.Utilities; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Text.Json; -using System.Text.RegularExpressions; - -/// -/// Collector implementation for East Devon District Council. -/// -internal sealed partial class EastDevonDistrictCouncil : GovUkCollectorBase, ICollector -{ - /// - public string Name => "East Devon District Council"; - - /// - public Uri WebsiteUrl => new("https://eastdevon.gov.uk/"); - - /// - public override string GovUkId => "east-devon"; - - /// - /// The list of bin types for this collector. - /// - private readonly IReadOnlyCollection _binTypes = - [ - new() - { - Name = "General Waste", - Colour = BinColour.Black, - Keys = [ "Rubbish" ], - }, - new() - { - Name = "Recycling (Paper/Glass/Cardboard)", - Colour = BinColour.Green, - Keys = [ "Recycling and food waste" ], - Type = BinType.Box, - }, - new() - { - Name = "Recycling (Plastics/Tins)", - Colour = BinColour.Green, - Keys = [ "Recycling and food waste" ], - Type = BinType.Sack, - }, - new() - { - Name = "Food Waste", - Colour = BinColour.Blue, - Keys = [ "Recycling and food waste" ], - Type = BinType.Caddy, - }, - new() - { - Name = "Garden Waste", - Colour = BinColour.Green, - Keys = [ "Green waste" ], - }, - ]; - - /// - /// Regex for parsing month headers and collection entries from the calendar. - /// - [GeneratedRegex(@"
  • ]*>

    (?[A-Za-z]+\s+\d{4})

  • |
  • (?[^<]+)(?.*?)
  • ", RegexOptions.IgnoreCase | RegexOptions.Singleline)] - private static partial Regex CollectionEntryRegex(); - - /// - /// Regex for extracting bin names from a collection entry. - /// - [GeneratedRegex(@"(?[^<]+)", RegexOptions.IgnoreCase)] - private static partial Regex BinNameRegex(); - - /// - public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) - { - // Prepare client-side request for getting addresses - if (clientSideResponse == null) - { - var encodedPostcode = WebUtility.UrlEncode(postcode); - - var clientSideRequest = new ClientSideRequest - { - RequestId = 1, - Url = $"https://eastdevon.gov.uk/addressfinder?qtype=bins&term={encodedPostcode}", - Method = "GET", - Headers = new() { - {"User-Agent", Constants.UserAgent}, - {"X-Requested-With", "XMLHttpRequest"}, - }, - }; - - var getAddressesResponse = new GetAddressesResponse - { - NextClientSideRequest = clientSideRequest - }; - - return getAddressesResponse; - } - // Process addresses from response - else if (clientSideResponse.RequestId == 1) - { - using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); - var addresses = new List
    (); - - // Iterate through each address, and create a new address object - foreach (var element in jsonDoc.RootElement.EnumerateArray()) - { - var property = element.GetProperty("label").GetString()!.Trim(); - var uprn = element.GetProperty("UPRN").GetString()!.Trim(); - - if (string.IsNullOrWhiteSpace(uprn)) - { - continue; - } - - var address = new Address - { - Property = property, - Postcode = postcode, - Uid = uprn, - }; - - addresses.Add(address); - } - - var getAddressesResponse = new GetAddressesResponse - { - Addresses = [.. addresses], - }; - - return getAddressesResponse; - } - - // Throw exception for invalid request - throw new InvalidOperationException("Invalid client-side request."); - } - - /// - public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? clientSideResponse) - { - // Prepare client-side request for getting bin days - if (clientSideResponse == null) - { - var clientSideRequest = new ClientSideRequest - { - RequestId = 1, - Url = $"https://eastdevon.gov.uk/recycling-and-waste/recycling-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN={address.Uid}", - Method = "GET", - Headers = new() { - {"User-Agent", Constants.UserAgent}, - }, - }; - - var getBinDaysResponse = new GetBinDaysResponse - { - NextClientSideRequest = clientSideRequest - }; - - return getBinDaysResponse; - } - // Process bin days from response - else if (clientSideResponse.RequestId == 1) - { - var content = clientSideResponse.Content; - - var currentMonth = string.Empty; - var binDays = new List(); - - // Iterate through each calendar entry, and build bin day objects - foreach (Match collectionEntry in CollectionEntryRegex().Matches(content)!) - { - var month = collectionEntry.Groups["month"].Value; - if (!string.IsNullOrWhiteSpace(month)) - { - currentMonth = WebUtility.HtmlDecode(month).Trim(); - continue; - } - - var dateText = WebUtility.HtmlDecode(collectionEntry.Groups["date"].Value).Trim(); - var day = dateText.Split(" ", StringSplitOptions.RemoveEmptyEntries)[0]; - - var date = DateOnly.ParseExact( - $"{day} {currentMonth}", - "d MMMM yyyy", - CultureInfo.InvariantCulture, - DateTimeStyles.None - ); - - var binsHtml = WebUtility.HtmlDecode(collectionEntry.Groups["bins"].Value); - var bins = new List(); - - // Iterate through each bin, and map to configured bin types - foreach (Match binMatch in BinNameRegex().Matches(binsHtml)!) - { - var binName = binMatch.Groups["bin"].Value.Trim(); - var matchedBins = ProcessingUtilities.GetMatchingBins(_binTypes, binName); - - bins.AddRange(matchedBins); - } - - var binDay = new BinDay - { - Date = date, - Address = address, - Bins = [.. bins], - }; - - binDays.Add(binDay); - } - - var getBinDaysResponse = new GetBinDaysResponse - { - BinDays = ProcessingUtilities.ProcessBinDays(binDays), - }; - - return getBinDaysResponse; - } - - // Throw exception for invalid request - throw new InvalidOperationException("Invalid client-side request."); - } -} +namespace BinDays.Api.Collectors.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors.Vendors; +using BinDays.Api.Collectors.Models; +using BinDays.Api.Collectors.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Text.Json; +using System.Text.RegularExpressions; + +/// +/// Collector implementation for East Devon District Council. +/// +internal sealed partial class EastDevonDistrictCouncil : GovUkCollectorBase, ICollector +{ + /// + public string Name => "East Devon District Council"; + + /// + public Uri WebsiteUrl => new("https://eastdevon.gov.uk/"); + + /// + public override string GovUkId => "east-devon"; + + /// + /// The list of bin types for this collector. + /// + private readonly IReadOnlyCollection _binTypes = + [ + new() + { + Name = "General Waste", + Colour = BinColour.Black, + Keys = [ "Rubbish" ], + }, + new() + { + Name = "Recycling (Paper/Glass/Cardboard)", + Colour = BinColour.Green, + Keys = [ "Recycling and food waste" ], + Type = BinType.Box, + }, + new() + { + Name = "Recycling (Plastics/Tins)", + Colour = BinColour.Green, + Keys = [ "Recycling and food waste" ], + Type = BinType.Sack, + }, + new() + { + Name = "Food Waste", + Colour = BinColour.Blue, + Keys = [ "Recycling and food waste" ], + Type = BinType.Caddy, + }, + new() + { + Name = "Garden Waste", + Colour = BinColour.Green, + Keys = [ "Green waste" ], + }, + ]; + + /// + /// Regex for parsing month headers and collection entries from the calendar. + /// + [GeneratedRegex(@"
  • ]*>

    (?[A-Za-z]+\s+\d{4})

  • |
  • (?[^<]+)(?.*?)
  • ", RegexOptions.IgnoreCase | RegexOptions.Singleline)] + private static partial Regex CollectionEntryRegex(); + + /// + /// Regex for extracting bin names from a collection entry. + /// + [GeneratedRegex(@"(?[^<]+)", RegexOptions.IgnoreCase)] + private static partial Regex BinNameRegex(); + + /// + public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting addresses + if (clientSideResponse == null) + { + var encodedPostcode = WebUtility.UrlEncode(postcode); + + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = $"https://eastdevon.gov.uk/addressfinder?qtype=bins&term={encodedPostcode}", + Method = "GET", + Headers = new() { + {"User-Agent", Constants.UserAgent}, + {"X-Requested-With", "XMLHttpRequest"}, + }, + }; + + var getAddressesResponse = new GetAddressesResponse + { + NextClientSideRequest = clientSideRequest + }; + + return getAddressesResponse; + } + // Process addresses from response + else if (clientSideResponse.RequestId == 1) + { + using var jsonDoc = JsonDocument.Parse(clientSideResponse.Content); + var addresses = new List
    (); + + // Iterate through each address, and create a new address object + foreach (var element in jsonDoc.RootElement.EnumerateArray()) + { + var property = element.GetProperty("label").GetString()!.Trim(); + var uprn = element.GetProperty("UPRN").GetString()!.Trim(); + + if (string.IsNullOrWhiteSpace(uprn)) + { + continue; + } + + var address = new Address + { + Property = property, + Postcode = postcode, + Uid = uprn, + }; + + addresses.Add(address); + } + + var getAddressesResponse = new GetAddressesResponse + { + Addresses = [.. addresses], + }; + + return getAddressesResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } + + /// + public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? clientSideResponse) + { + // Prepare client-side request for getting bin days + if (clientSideResponse == null) + { + var clientSideRequest = new ClientSideRequest + { + RequestId = 1, + Url = $"https://eastdevon.gov.uk/recycling-and-waste/recycling-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN={address.Uid}", + Method = "GET", + Headers = new() { + {"User-Agent", Constants.UserAgent}, + }, + }; + + var getBinDaysResponse = new GetBinDaysResponse + { + NextClientSideRequest = clientSideRequest + }; + + return getBinDaysResponse; + } + // Process bin days from response + else if (clientSideResponse.RequestId == 1) + { + var content = clientSideResponse.Content; + + var currentMonth = string.Empty; + var binDays = new List(); + + // Iterate through each calendar entry, and build bin day objects + foreach (Match collectionEntry in CollectionEntryRegex().Matches(content)!) + { + var month = collectionEntry.Groups["month"].Value; + if (!string.IsNullOrWhiteSpace(month)) + { + currentMonth = WebUtility.HtmlDecode(month).Trim(); + continue; + } + + var dateText = WebUtility.HtmlDecode(collectionEntry.Groups["date"].Value).Trim(); + var day = dateText.Split(" ", StringSplitOptions.RemoveEmptyEntries)[0]; + + var date = DateOnly.ParseExact( + $"{day} {currentMonth}", + "d MMMM yyyy", + CultureInfo.InvariantCulture, + DateTimeStyles.None + ); + + var binsHtml = WebUtility.HtmlDecode(collectionEntry.Groups["bins"].Value); + var bins = new List(); + + // Iterate through each bin, and map to configured bin types + foreach (Match binMatch in BinNameRegex().Matches(binsHtml)!) + { + var binName = binMatch.Groups["bin"].Value.Trim(); + var matchedBins = ProcessingUtilities.GetMatchingBins(_binTypes, binName); + + bins.AddRange(matchedBins); + } + + var binDay = new BinDay + { + Date = date, + Address = address, + Bins = [.. bins], + }; + + binDays.Add(binDay); + } + + var getBinDaysResponse = new GetBinDaysResponse + { + BinDays = ProcessingUtilities.ProcessBinDays(binDays), + }; + + return getBinDaysResponse; + } + + // Throw exception for invalid request + throw new InvalidOperationException("Invalid client-side request."); + } +} diff --git a/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs b/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs index 6595b25..ef2b273 100644 --- a/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs +++ b/BinDays.Api.IntegrationTests/Collectors/Councils/EastDevonDistrictCouncilTests.cs @@ -1,36 +1,36 @@ -namespace BinDays.Api.IntegrationTests.Collectors.Councils; - -using BinDays.Api.Collectors.Collectors; -using BinDays.Api.Collectors.Collectors.Councils; -using BinDays.Api.Collectors.Services; -using BinDays.Api.IntegrationTests.Helpers; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -public class EastDevonDistrictCouncilTests -{ - private readonly IntegrationTestClient _client; - private static readonly ICollector _collector = new EastDevonDistrictCouncil(); - private readonly CollectorService _collectorService = new([_collector]); - private readonly ITestOutputHelper _outputHelper; - - public EastDevonDistrictCouncilTests(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - _client = new IntegrationTestClient(outputHelper); - } - - [Theory] - [InlineData("EX10 8UB")] - public async Task GetBinDaysTest(string postcode) - { - await TestSteps.EndToEnd( - _client, - _collectorService, - _collector, - postcode, - _outputHelper - ); - } -} +namespace BinDays.Api.IntegrationTests.Collectors.Councils; + +using BinDays.Api.Collectors.Collectors; +using BinDays.Api.Collectors.Collectors.Councils; +using BinDays.Api.Collectors.Services; +using BinDays.Api.IntegrationTests.Helpers; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +public class EastDevonDistrictCouncilTests +{ + private readonly IntegrationTestClient _client; + private static readonly ICollector _collector = new EastDevonDistrictCouncil(); + private readonly CollectorService _collectorService = new([_collector]); + private readonly ITestOutputHelper _outputHelper; + + public EastDevonDistrictCouncilTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _client = new IntegrationTestClient(outputHelper); + } + + [Theory] + [InlineData("EX10 8UB")] + public async Task GetBinDaysTest(string postcode) + { + await TestSteps.EndToEnd( + _client, + _collectorService, + _collector, + postcode, + _outputHelper + ); + } +} From 8dffaed886e71fb0717c4c649aac02b3a885b248 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:05:35 +0000 Subject: [PATCH 3/4] Address PR review comments for EastDevonDistrictCouncil - Update bin names to be more descriptive (Paper, Glass & Cardboard Recycling) - Remove unnecessary postcode URL encoding - Remove unnecessary content variable - Add trailing commas and proper formatting for Headers dictionaries - Replace fragile Split-based date parsing with Regex for robustness Co-authored-by: Andrew Riggs --- .../Councils/EastDevonDistrictCouncil.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs index 68533bd..72aeb08 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs @@ -37,14 +37,14 @@ internal sealed partial class EastDevonDistrictCouncil : GovUkCollectorBase, ICo }, new() { - Name = "Recycling (Paper/Glass/Cardboard)", + Name = "Paper, Glass & Cardboard Recycling", Colour = BinColour.Green, Keys = [ "Recycling and food waste" ], Type = BinType.Box, }, new() { - Name = "Recycling (Plastics/Tins)", + Name = "Plastics & Tins Recycling", Colour = BinColour.Green, Keys = [ "Recycling and food waste" ], Type = BinType.Sack, @@ -76,20 +76,25 @@ internal sealed partial class EastDevonDistrictCouncil : GovUkCollectorBase, ICo [GeneratedRegex(@"(?[^<]+)", RegexOptions.IgnoreCase)] private static partial Regex BinNameRegex(); + /// + /// Regex for extracting the day number from a date string. + /// + [GeneratedRegex(@"\d+")] + private static partial Regex DayNumberRegex(); + /// public GetAddressesResponse GetAddresses(string postcode, ClientSideResponse? clientSideResponse) { // Prepare client-side request for getting addresses if (clientSideResponse == null) { - var encodedPostcode = WebUtility.UrlEncode(postcode); - var clientSideRequest = new ClientSideRequest { RequestId = 1, - Url = $"https://eastdevon.gov.uk/addressfinder?qtype=bins&term={encodedPostcode}", + Url = $"https://eastdevon.gov.uk/addressfinder?qtype=bins&term={postcode}", Method = "GET", - Headers = new() { + Headers = new() + { {"User-Agent", Constants.UserAgent}, {"X-Requested-With", "XMLHttpRequest"}, }, @@ -152,7 +157,8 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client RequestId = 1, Url = $"https://eastdevon.gov.uk/recycling-and-waste/recycling-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN={address.Uid}", Method = "GET", - Headers = new() { + Headers = new() + { {"User-Agent", Constants.UserAgent}, }, }; @@ -167,13 +173,11 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client // Process bin days from response else if (clientSideResponse.RequestId == 1) { - var content = clientSideResponse.Content; - var currentMonth = string.Empty; var binDays = new List(); // Iterate through each calendar entry, and build bin day objects - foreach (Match collectionEntry in CollectionEntryRegex().Matches(content)!) + foreach (Match collectionEntry in CollectionEntryRegex().Matches(clientSideResponse.Content)!) { var month = collectionEntry.Groups["month"].Value; if (!string.IsNullOrWhiteSpace(month)) @@ -183,7 +187,7 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client } var dateText = WebUtility.HtmlDecode(collectionEntry.Groups["date"].Value).Trim(); - var day = dateText.Split(" ", StringSplitOptions.RemoveEmptyEntries)[0]; + var day = DayNumberRegex().Match(dateText).Value; var date = DateOnly.ParseExact( $"{day} {currentMonth}", From 640e9bb201e90bc7a4bd76cf8cf5e2dd13ae11ac Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:14:11 +0000 Subject: [PATCH 4/4] Use ParseDateInferringYear for more robust date parsing - Updated regex to capture only month name (year optional) - Replaced DateOnly.ParseExact with ParseDateInferringYear extension - Handles year transitions correctly (e.g., Dec -> Jan boundary) - More resilient to website HTML format changes - Follows style guide recommendation for date parsing Co-authored-by: Andrew Riggs --- .../Collectors/Councils/EastDevonDistrictCouncil.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs index 72aeb08..6fb3710 100644 --- a/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs +++ b/BinDays.Api.Collectors/Collectors/Councils/EastDevonDistrictCouncil.cs @@ -67,7 +67,7 @@ internal sealed partial class EastDevonDistrictCouncil : GovUkCollectorBase, ICo /// /// Regex for parsing month headers and collection entries from the calendar. /// - [GeneratedRegex(@"
  • ]*>

    (?[A-Za-z]+\s+\d{4})

  • |
  • (?[^<]+)(?.*?)
  • ", RegexOptions.IgnoreCase | RegexOptions.Singleline)] + [GeneratedRegex(@"
  • ]*>

    (?[A-Za-z]+)(?:\s+\d{4})?

  • |
  • (?[^<]+)(?.*?)
  • ", RegexOptions.IgnoreCase | RegexOptions.Singleline)] private static partial Regex CollectionEntryRegex(); /// @@ -189,12 +189,7 @@ public GetBinDaysResponse GetBinDays(Address address, ClientSideResponse? client var dateText = WebUtility.HtmlDecode(collectionEntry.Groups["date"].Value).Trim(); var day = DayNumberRegex().Match(dateText).Value; - var date = DateOnly.ParseExact( - $"{day} {currentMonth}", - "d MMMM yyyy", - CultureInfo.InvariantCulture, - DateTimeStyles.None - ); + var date = $"{day} {currentMonth}".ParseDateInferringYear("d MMMM"); var binsHtml = WebUtility.HtmlDecode(collectionEntry.Groups["bins"].Value); var bins = new List();