Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vs/
bin/
obj/
*.user
30 changes: 30 additions & 0 deletions PowerplantCodingChallenge/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using PowerplantCodingChallenge.Business.Models;

namespace PowerplantCodingChallenge.Business.Extensions
{
internal static class CollectionExtensions
{
internal static IEnumerable<AllocationCandidate> SortByMeritOrder(this IEnumerable<AllocationCandidate> candidates)
=> candidates
.OrderBy(c => c.MarginalCost)
.ThenBy(c => c.PowerMaxTenths);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PowerplantCodingChallenge.Business.Extensions
{
internal static class NumericExtensions
{
internal static int ToTenths(this decimal value)
{
return (int)Math.Round(value * 10m, MidpointRounding.AwayFromZero);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using PowerplantCodingChallenge.Business.Extensions;
using PowerplantCodingChallenge.Business.Models;
using PowerplantCodingChallenge.Domain.Request;
using PowerplantCodingChallenge.Domain.Request.Enumeration;

namespace PowerplantCodingChallenge.Business.Factories
{
internal static class AllocationCandidateFactory
{
public static List<AllocationCandidate> CreateCandidates(ProductionPlanRequest request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (request.Fuels == null)
{
throw new ArgumentNullException(nameof(request.Fuels));
}
if (request.PowerPlants == null)
{
throw new ArgumentNullException(nameof(request.PowerPlants));
}

return request.PowerPlants.Select(pp => CreateCandidate(pp, request.Fuels)).ToList();
}

private static AllocationCandidate CreateCandidate(PowerPlant powerPlant, FuelPrices fuels)
=> new AllocationCandidate
{
Name = powerPlant.Name,
Type = powerPlant.Type,
MarginalCost = GetMarginalCost(powerPlant, fuels),
PowerMinTenths = powerPlant.PowerMin.ToTenths(),
PowerMaxTenths = GetAvailablePowerMax(powerPlant, fuels).ToTenths(),
AllocatedPowerTenths = 0
};

private static decimal GetAvailablePowerMax(PowerPlant powerPlant, FuelPrices fuels)
=> powerPlant.Type switch
{
PlantType.WindTurbine => powerPlant.PowerMax * fuels.WindPercentage / 100m,
_ => powerPlant.PowerMax
};

private static decimal GetMarginalCost(PowerPlant powerPlant, FuelPrices fuels)
=> powerPlant.Type switch
{
PlantType.WindTurbine => 0m,
PlantType.GasFired => fuels.GasEuroPerMWh / powerPlant.Efficiency,
PlantType.TurboJet => fuels.KerosineEuroPerMWh / powerPlant.Efficiency,
_ => throw new InvalidOperationException($"Unknown plant type : {powerPlant.Type}")
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using PowerplantCodingChallenge.Domain.Request.Enumeration;

namespace PowerplantCodingChallenge.Business.Models
{
internal sealed class AllocationCandidate
{
public string Name { get; init; } = string.Empty;

public PlantType Type { get; init; }

/// <summary>
/// Cost per MWh, used for sorting by merit-order
/// </summary>
public decimal MarginalCost { get; init; }

/// <summary>
/// Minimum production if plant is active (in tenths of MW)
/// </summary>
public int PowerMinTenths { get; init; }

/// <summary>
/// Maximum available production (in tenths of MW)
/// </summary>
public int PowerMaxTenths { get; init; }

/// <summary>
/// Computed allocated production (in tenths of MW)
/// </summary>
public int AllocatedPowerTenths { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace PowerplantCodingChallenge.Business.Models.Exceptions
{
public sealed class BusinessException : Exception
{
public BusinessException(string message) : base(message) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PowerplantCodingChallenge.Domain\PowerplantCodingChallenge.Domain.csproj" />
<ProjectReference Include="..\PowerplantCodingChallenge.IBusiness\PowerplantCodingChallenge.IBusiness.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Logging;
using PowerplantCodingChallenge.Business.Extensions;
using PowerplantCodingChallenge.Business.Factories;
using PowerplantCodingChallenge.Business.Models.Exceptions;
using PowerplantCodingChallenge.Domain.Request;
using PowerplantCodingChallenge.Domain.Response;
using PowerplantCodingChallenge.IBusiness;

namespace PowerplantCodingChallenge.Business
{
public sealed class ProductionPlanService : IProductionPlanService
{
private readonly ILogger<ProductionPlanService> _logger;

public ProductionPlanService(ILogger<ProductionPlanService> logger)
{
_logger = logger;
}

public IEnumerable<ProductionPlanItem> GetProductionPlan(ProductionPlanRequest request)
{
var candidates = AllocationCandidateFactory.CreateCandidates(request);
var sorted = candidates.SortByMeritOrder();
var loadTenths = request.Load.ToTenths();
var totalMin = 0;

foreach (var candidate in sorted)
{
candidate.AllocatedPowerTenths = candidate.PowerMinTenths;
totalMin += candidate.PowerMinTenths;
}

if (totalMin > loadTenths)
{
throw new BusinessException("Load cannot be satisfied due to minimum production constraints.");
}

int remaining = loadTenths - totalMin;

foreach (var candidate in sorted)
{
if (remaining <= 0)
break;

int available = candidate.PowerMaxTenths - candidate.AllocatedPowerTenths;
int toAdd = Math.Min(available, remaining);

candidate.AllocatedPowerTenths += toAdd;
remaining -= toAdd;
}

if (remaining > 0)
{
throw new BusinessException("Unable to meet required load.");
}

return candidates
.Select(c => new ProductionPlanItem
{
Name = c.Name,
Power = (decimal)c.AllocatedPowerTenths / 10
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace PowerplantCodingChallenge.Domain.Request.Enumeration
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PlantType
{
[JsonStringEnumMemberName("gasfired")] GasFired = 0,
[JsonStringEnumMemberName("turbojet")] TurboJet = 1,
[JsonStringEnumMemberName("windturbine")] WindTurbine = 2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;

namespace PowerplantCodingChallenge.Domain.Request
{
public sealed class FuelPrices
{
[JsonPropertyName("gas(euro/MWh)")]
public decimal GasEuroPerMWh { get; init; }

[JsonPropertyName("kerosine(euro/MWh)")]
public decimal KerosineEuroPerMWh { get; init; }

[JsonPropertyName("co2(euro/ton)")]
public decimal Co2EuroPerTon { get; init; }

[JsonPropertyName("wind(%)")]
public decimal WindPercentage { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using PowerplantCodingChallenge.Domain.Request.Enumeration;
using System.Text.Json.Serialization;

namespace PowerplantCodingChallenge.Domain.Request
{
public sealed class PowerPlant
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;

[JsonPropertyName("type")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public PlantType Type { get; init; }

[JsonPropertyName("efficiency")]
public decimal Efficiency { get; init; }

[JsonPropertyName("pmin")]
public decimal PowerMin { get; init; }

[JsonPropertyName("pmax")]
public decimal PowerMax { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;

namespace PowerplantCodingChallenge.Domain.Request
{
public sealed class ProductionPlanRequest
{
[JsonPropertyName("load")]
public decimal Load { get; init; }

[JsonPropertyName("fuels")]
public FuelPrices Fuels { get; init; } = new();

[JsonPropertyName("powerplants")]
public List<PowerPlant> PowerPlants { get; init; } = [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace PowerplantCodingChallenge.Domain.Response
{
public sealed class ProductionPlanItem
{
[JsonPropertyName("name")]
public string Name { get; init; } = string.Empty;

[JsonPropertyName("p")]
public decimal Power { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using PowerplantCodingChallenge.Domain.Request;
using PowerplantCodingChallenge.Domain.Response;

namespace PowerplantCodingChallenge.IBusiness
{
public interface IProductionPlanService
{
IEnumerable<ProductionPlanItem> GetProductionPlan(ProductionPlanRequest request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\PowerplantCodingChallenge.Domain\PowerplantCodingChallenge.Domain.csproj" />
</ItemGroup>

</Project>
Loading