C# & ASP.NET Topic
The Barber Simulation
A barber can serve only one customer at a time. It takes him 15–20 minutes to cut a man’s hair and 30–60 minutes for a woman’s haircut. The barber has four waiting chairs in his shop. Every 15–30 minutes, a new customer arrives. If no waiting chair is available, the barber loses the customer. Otherwise, the customer takes a seat in the waiting area until it’s their turn. The barber’s working hours are from 9:00 a.m. to 6:00 p.m. Develop a simulation (no graphical display, numbers only) that represents a random daily routine of the barber, with output showing every event or change that occurs.
Here is the Barber Simulation in C#. This will be a time-based discrete event simulation.
Project Structure (Best Practices)
text
BarberShopSimulation/
│
├── BarberShopSimulation.sln # Solution file
│
├── BarberShopSimulation/
│ │
│ ├── Program.cs # Entry point
│ │
│ ├── Core/ # Domain/business logic
│ │ ├── Models/
│ │ │ ├── Customer.cs
│ │ │ ├── BarberShop.cs
│ │ │ └── SimulationConfig.cs
│ │ │
│ │ ├── Services/
│ │ │ ├── ISimulationService.cs
│ │ │ ├── SimulationService.cs
│ │ │ └── ITimeService.cs
│ │ │
│ │ └── Enums/
│ │ └── Gender.cs
│ │
│ ├── Infrastructure/ # Infrastructure concerns
│ │ ├── RandomNumberGenerator.cs
│ │ └── TimeProvider.cs
│ │
│ ├── Simulation/ # Simulation logic
│ │ ├── EventSimulator.cs
│ │ ├── EventLogger.cs
│ │ └── StatisticsCalculator.cs
│ │
│ ├── Utils/ # Utilities
│ │ └── ConsoleExtensions.cs
│ │
│ ├── appsettings.json # Configuration
│ └── BarberShopSimulation.csproj # Project file
│
└── README.md # Documentation
Step-by-Step Implementation
Step 1: Create the Solution and Project
bash
# Create directory
mkdir BarberShopSimulation
cd BarberShopSimulation
# Create solution
dotnet new sln
# Create console project
dotnet new console -n BarberShopSimulation
# Add project to solution
dotnet sln add BarberShopSimulation/BarberShopSimulation.csproj
Step 2: Update Project File (BarberShopSimulation.csproj)
xml
Run
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AssemblyName>BarberShopSimulation</AssemblyName>
<RootNamespace>BarberShopSimulation</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Step 3: Create Folder Structure
bash
cd BarberShopSimulation
# Create directories
mkdir Core
mkdir Core/Models
mkdir Core/Services
mkdir Core/Enums
mkdir Infrastructure
mkdir Simulation
mkdir Utils
Step 4: Implement the Code Files
4.1 Core/Enums/Gender.cs
csharp
namespace BarberShopSimulation.Core.Enums
{
public enum Gender
{
Male,
Female
}
}
4.2 Core/Models/SimulationConfig.cs
csharp
namespace BarberShopSimulation.Core.Models
{
public class SimulationConfig
{
public TimeSpan OpenTime { get; set; } = new TimeSpan(9, 0, 0); // 9:00 AM
public TimeSpan CloseTime { get; set; } = new TimeSpan(18, 0, 0); // 6:00 PM
public int MaxWaitingChairs { get; set; } = 4;
public int MinArrivalMinutes { get; set; } = 15;
public int MaxArrivalMinutes { get; set; } = 30;
public int MinMaleServiceMinutes { get; set; } = 15;
public int MaxMaleServiceMinutes { get; set; } = 20;
public int MinFemaleServiceMinutes { get; set; } = 30;
public int MaxFemaleServiceMinutes { get; set; } = 60;
public double MaleCustomerProbability { get; set; } = 0.5; // 50% chance
}
}
4.3 Core/Models/Customer.cs
csharp
using BarberShopSimulation.Core.Enums;
namespace BarberShopSimulation.Core.Models
{
public class Customer
{
public int Id { get; set; }
public Gender Gender { get; set; }
public DateTime ArrivalTime { get; set; }
public DateTime? ServiceStartTime { get; set; }
public DateTime? ServiceEndTime { get; set; }
public bool WasServed => ServiceStartTime.HasValue;
public bool WasLost { get; set; }
public TimeSpan WaitTime => ServiceStartTime.HasValue
? ServiceStartTime.Value - ArrivalTime
: TimeSpan.Zero;
public TimeSpan ServiceTime => (ServiceStartTime.HasValue && ServiceEndTime.HasValue)
? ServiceEndTime.Value - ServiceStartTime.Value
: TimeSpan.Zero;
public TimeSpan TotalTimeInSystem => ServiceEndTime.HasValue
? ServiceEndTime.Value - ArrivalTime
: TimeSpan.Zero;
}
}
4.4 Core/Models/SimulationResult.cs
csharp
namespace BarberShopSimulation.Core.Models
{
public class SimulationResult
{
public int TotalCustomersArrived { get; set; }
public int TotalCustomersServed { get; set; }
public int TotalCustomersLost { get; set; }
public TimeSpan AverageWaitTime { get; set; }
public TimeSpan AverageServiceTime { get; set; }
public TimeSpan AverageMaleServiceTime { get; set; }
public TimeSpan AverageFemaleServiceTime { get; set; }
public DateTime SimulationEndTime { get; set; }
public List<Customer> ServedCustomers { get; set; } = new();
public List<Customer> LostCustomers { get; set; } = new();
public double LostCustomerPercentage => TotalCustomersArrived > 0
? (TotalCustomersLost * 100.0) / TotalCustomersArrived
: 0;
public double ChairUtilizationPercentage { get; set; }
}
}
4.5 Infrastructure/RandomNumberGenerator.cs
csharp
namespace BarberShopSimulation.Infrastructure
{
public interface IRandomNumberGenerator
{
int Next(int minValue, int maxValue);
double NextDouble();
}
public class RandomNumberGenerator : IRandomNumberGenerator
{
private readonly Random _random = new();
private readonly object _lock = new();
public int Next(int minValue, int maxValue)
{
lock (_lock)
{
return _random.Next(minValue, maxValue);
}
}
public double NextDouble()
{
lock (_lock)
{
return _random.NextDouble();
}
}
}
}
4.6 Infrastructure/TimeProvider.cs
csharp
namespace BarberShopSimulation.Infrastructure
{
public interface ITimeProvider
{
DateTime Now { get; }
}
public class SystemTimeProvider : ITimeProvider
{
public DateTime Now => DateTime.Now;
}
public class FixedTimeProvider : ITimeProvider
{
public DateTime Now { get; private set; }
public FixedTimeProvider(DateTime fixedTime)
{
Now = fixedTime;
}
public void Advance(TimeSpan timeSpan)
{
Now = Now.Add(timeSpan);
}
}
}
4.7 Core/Services/ISimulationService.cs
csharp
using BarberShopSimulation.Core.Models;
namespace BarberShopSimulation.Core.Services
{
public interface ISimulationService
{
SimulationResult RunSimulation(SimulationConfig config);
void RunSimulationWithEvents(SimulationConfig config, Action<string> onEvent);
}
}
4.8 Core/Services/SimulationService.cs
csharp
using BarberShopSimulation.Core.Enums;
using BarberShopSimulation.Core.Models;
using BarberShopSimulation.Infrastructure;
namespace BarberShopSimulation.Core.Services
{
public class SimulationService : ISimulationService
{
private readonly IRandomNumberGenerator _random;
private readonly ITimeProvider _timeProvider;
public SimulationService(IRandomNumberGenerator random, ITimeProvider timeProvider)
{
_random = random;
_timeProvider = timeProvider;
}
public SimulationResult RunSimulation(SimulationConfig config)
{
var result = new SimulationResult();
var servedCustomers = new List<Customer>();
var lostCustomers = new List<Customer>();
var currentTime = new DateTime(_timeProvider.Now.Year, _timeProvider.Now.Month,
_timeProvider.Now.Day, 0, 0, 0)
.Add(config.OpenTime);
var shopCloseTime = new DateTime(_timeProvider.Now.Year, _timeProvider.Now.Month,
_timeProvider.Now.Day, 0, 0, 0)
.Add(config.CloseTime);
var queue = new Queue<Customer>();
Customer? currentCustomer = null;
DateTime? currentServiceEnd = null;
DateTime? nextArrivalTime = ScheduleNextArrival(currentTime, config);
int customerId = 1;
while (currentTime <= shopCloseTime || currentCustomer != null || queue.Count > 0)
{
// Handle customer arrivals
if (nextArrivalTime.HasValue && currentTime >= nextArrivalTime.Value && currentTime <= shopCloseTime)
{
var customer = CreateCustomer(customerId++, config, nextArrivalTime.Value);
result.TotalCustomersArrived++;
if (currentCustomer == null && queue.Count == 0)
{
// Barber is free
currentCustomer = customer;
currentServiceEnd = StartService(currentCustomer, currentTime, config);
}
else if (queue.Count < config.MaxWaitingChairs)
{
// Add to queue
queue.Enqueue(customer);
}
else
{
// No chairs available
customer.WasLost = true;
lostCustomers.Add(customer);
result.TotalCustomersLost++;
}
nextArrivalTime = ScheduleNextArrival(currentTime, config);
}
// Handle service completion
if (currentCustomer != null && currentServiceEnd.HasValue && currentTime >= currentServiceEnd.Value)
{
currentCustomer.ServiceEndTime = currentServiceEnd.Value;
servedCustomers.Add(currentCustomer);
result.TotalCustomersServed++;
// Start next customer from queue
if (queue.Count > 0)
{
currentCustomer = queue.Dequeue();
currentCustomer.ServiceStartTime = currentTime;
currentServiceEnd = StartService(currentCustomer, currentTime, config);
}
else
{
currentCustomer = null;
currentServiceEnd = null;
}
}
// Advance time to next event
currentTime = GetNextEventTime(currentTime, nextArrivalTime, currentServiceEnd, shopCloseTime);
}
// Calculate statistics
result.ServedCustomers = servedCustomers;
result.LostCustomers = lostCustomers;
result.TotalCustomersServed = servedCustomers.Count;
result.TotalCustomersLost = lostCustomers.Count;
result.SimulationEndTime = currentTime;
if (servedCustomers.Count > 0)
{
result.AverageWaitTime = TimeSpan.FromMilliseconds(
servedCustomers.Average(c => c.WaitTime.TotalMilliseconds));
result.AverageServiceTime = TimeSpan.FromMilliseconds(
servedCustomers.Average(c => c.ServiceTime.TotalMilliseconds));
var maleCustomers = servedCustomers.Where(c => c.Gender == Gender.Male).ToList();
var femaleCustomers = servedCustomers.Where(c => c.Gender == Gender.Female).ToList();
if (maleCustomers.Count > 0)
{
result.AverageMaleServiceTime = TimeSpan.FromMilliseconds(
maleCustomers.Average(c => c.ServiceTime.TotalMilliseconds));
}
if (femaleCustomers.Count > 0)
{
result.AverageFemaleServiceTime = TimeSpan.FromMilliseconds(
femaleCustomers.Average(c => c.ServiceTime.TotalMilliseconds));
}
}
return result;
}
private DateTime? ScheduleNextArrival(DateTime currentTime, SimulationConfig config)
{
int minutes = _random.Next(config.MinArrivalMinutes, config.MaxArrivalMinutes + 1);
return currentTime.AddMinutes(minutes);
}
private Customer CreateCustomer(int id, SimulationConfig config, DateTime arrivalTime)
{
var gender = _random.NextDouble() < config.MaleCustomerProbability
? Gender.Male
: Gender.Female;
return new Customer
{
Id = id,
Gender = gender,
ArrivalTime = arrivalTime
};
}
private DateTime StartService(Customer customer, DateTime startTime, SimulationConfig config)
{
customer.ServiceStartTime = startTime;
int serviceMinutes = customer.Gender == Gender.Male
? _random.Next(config.MinMaleServiceMinutes, config.MaxMaleServiceMinutes + 1)
: _random.Next(config.MinFemaleServiceMinutes, config.MaxFemaleServiceMinutes + 1);
return startTime.AddMinutes(serviceMinutes);
}
private DateTime GetNextEventTime(DateTime currentTime, DateTime? nextArrival,
DateTime? nextServiceEnd, DateTime shopCloseTime)
{
var possibleTimes = new List<DateTime>();
if (nextArrival.HasValue && nextArrival.Value > currentTime)
possibleTimes.Add(nextArrival.Value);
if (nextServiceEnd.HasValue && nextServiceEnd.Value > currentTime)
possibleTimes.Add(nextServiceEnd.Value);
if (currentTime < shopCloseTime)
possibleTimes.Add(shopCloseTime);
return possibleTimes.Count > 0 ? possibleTimes.Min() : currentTime.AddMinutes(1);
}
public void RunSimulationWithEvents(SimulationConfig config, Action<string> onEvent)
{
// Implementation with event logging
var result = RunSimulation(config);
// You could enhance this to log individual events
onEvent?.Invoke("Simulation completed");
onEvent?.Invoke($"Total customers: {result.TotalCustomersArrived}");
onEvent?.Invoke($"Served: {result.TotalCustomersServed}, Lost: {result.TotalCustomersLost}");
}
}
}
4.9 Simulation/EventLogger.cs
csharp
namespace BarberShopSimulation.Simulation
{
public class EventLogger
{
public void LogEvent(string message)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
public void LogSimulationEvent(DateTime simulationTime, string message)
{
Console.WriteLine($"[{simulationTime:HH:mm}] {message}");
}
public void LogSectionHeader(string header)
{
Console.WriteLine("\n" + new string('=', 50));
Console.WriteLine(header);
Console.WriteLine(new string('=', 50));
}
public void LogStatistics(string label, string value)
{
Console.WriteLine($"{label,-30}: {value}");
}
}
}
4.10 Simulation/StatisticsCalculator.cs
csharp
using BarberShopSimulation.Core.Models;
namespace BarberShopSimulation.Simulation
{
public class StatisticsCalculator
{
public static SimulationResult CalculateDetailedStatistics(SimulationResult result)
{
if (result.ServedCustomers.Count == 0)
return result;
// Calculate additional statistics
var waitTimes = result.ServedCustomers.Select(c => c.WaitTime.TotalMinutes).ToList();
var serviceTimes = result.ServedCustomers.Select(c => c.ServiceTime.TotalMinutes).ToList();
// You could add more statistical calculations here:
// - Standard deviation
// - Median
// - Percentiles
// - Maximum wait time
// - Queue length distribution
return result;
}
}
}
4.11 Utils/ConsoleExtensions.cs
csharp
namespace BarberShopSimulation.Utils
{
public static class ConsoleExtensions
{
public static void WriteWithColor(string text, ConsoleColor color)
{
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.Write(text);
Console.ForegroundColor = originalColor;
}
public static void WriteLineWithColor(string text, ConsoleColor color)
{
var originalColor = Console.ForegroundColor;
Console.ForegroundColor = color;
Console.WriteLine(text);
Console.ForegroundColor = originalColor;
}
public static void WriteSuccess(string text) => WriteLineWithColor(text, ConsoleColor.Green);
public static void WriteWarning(string text) => WriteLineWithColor(text, ConsoleColor.Yellow);
public static void WriteError(string text) => WriteLineWithColor(text, ConsoleColor.Red);
public static void WriteInfo(string text) => WriteLineWithColor(text, ConsoleColor.Cyan);
public static string ReadString(string prompt, string defaultValue = "")
{
Console.Write($"{prompt} [{defaultValue}]: ");
var input = Console.ReadLine();
return string.IsNullOrWhiteSpace(input) ? defaultValue : input;
}
public static int ReadInt(string prompt, int defaultValue, int min = int.MinValue, int max = int.MaxValue)
{
while (true)
{
Console.Write($"{prompt} [{defaultValue}]: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
return defaultValue;
if (int.TryParse(input, out int result) && result >= min && result <= max)
return result;
Console.WriteLine($"Please enter a number between {min} and {max}");
}
}
}
}
4.12 appsettings.json
json
{
"SimulationConfig": {
"OpenTime": "09:00:00",
"CloseTime": "18:00:00",
"MaxWaitingChairs": 4,
"MinArrivalMinutes": 15,
"MaxArrivalMinutes": 30,
"MinMaleServiceMinutes": 15,
"MaxMaleServiceMinutes": 20,
"MinFemaleServiceMinutes": 30,
"MaxFemaleServiceMinutes": 60,
"MaleCustomerProbability": 0.5
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
4.13 Program.cs (Main Entry Point)
csharp
using BarberShopSimulation.Core.Models;
using BarberShopSimulation.Core.Services;
using BarberShopSimulation.Infrastructure;
using BarberShopSimulation.Simulation;
using BarberShopSimulation.Utils;
using Microsoft.Extensions.Configuration;
// Setup configuration
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
// Setup dependencies
var randomGenerator = new RandomNumberGenerator();
var timeProvider = new SystemTimeProvider();
var simulationService = new SimulationService(randomGenerator, timeProvider);
var eventLogger = new EventLogger();
// Load configuration
var config = new SimulationConfig();
configuration.GetSection("SimulationConfig").Bind(config);
// Display header
Console.Clear();
eventLogger.LogSectionHeader("BARBER SHOP SIMULATION");
Console.WriteLine("Welcome to the Barber Shop Simulation!");
Console.WriteLine("This simulation models a day in the life of a barber shop.");
Console.WriteLine();
// Optional: Allow user to customize settings
Console.WriteLine("Press Enter to use default settings, or customize below:");
Console.WriteLine();
if (ConsoleExtensions.ReadString("Customize settings? (y/n)", "n").ToLower() == "y")
{
config.MaxWaitingChairs = ConsoleExtensions.ReadInt("Number of waiting chairs", config.MaxWaitingChairs, 1, 10);
config.MinArrivalMinutes = ConsoleExtensions.ReadInt("Minimum minutes between arrivals", config.MinArrivalMinutes, 1, 60);
config.MaxArrivalMinutes = ConsoleExtensions.ReadInt("Maximum minutes between arrivals", config.MaxArrivalMinutes, config.MinArrivalMinutes, 120);
config.MaleCustomerProbability = ConsoleExtensions.ReadInt("Percentage of male customers", (int)(config.MaleCustomerProbability * 100), 0, 100) / 100.0;
}
// Run simulation
eventLogger.LogSectionHeader("SIMULATION STARTING");
Console.WriteLine($"Shop hours: {config.OpenTime:hh\\:mm} - {config.CloseTime:hh\\:mm}");
Console.WriteLine($"Waiting chairs: {config.MaxWaitingChairs}");
Console.WriteLine($"Customer arrival: every {config.MinArrivalMinutes}-{config.MaxArrivalMinutes} minutes");
Console.WriteLine();
Console.WriteLine("Running simulation...");
Console.WriteLine();
var result = simulationService.RunSimulation(config);
// Display results
eventLogger.LogSectionHeader("SIMULATION RESULTS");
eventLogger.LogStatistics("Total customers arrived", result.TotalCustomersArrived.ToString());
eventLogger.LogStatistics("Total customers served", result.TotalCustomersServed.ToString());
eventLogger.LogStatistics("Total customers lost", result.TotalCustomersLost.ToString());
eventLogger.LogStatistics("Lost customer percentage", $"{result.LostCustomerPercentage:F1}%");
if (result.ServedCustomers.Count > 0)
{
Console.WriteLine();
eventLogger.LogStatistics("Average wait time", $"{result.AverageWaitTime:mm\\:ss} minutes");
eventLogger.LogStatistics("Average service time", $"{result.AverageServiceTime:mm\\:ss} minutes");
eventLogger.LogStatistics("Average male service time", $"{result.AverageMaleServiceTime:mm\\:ss} minutes");
eventLogger.LogStatistics("Average female service time", $"{result.AverageFemaleServiceTime:mm\\:ss} minutes");
// Show some customer details
Console.WriteLine();
Console.WriteLine("Last 5 customers served:");
foreach (var customer in result.ServedCustomers.TakeLast(5))
{
Console.WriteLine($" Customer #{customer.Id} ({customer.Gender}): " +
$"Arrived {customer.ArrivalTime:HH:mm}, " +
$"Waited {customer.WaitTime:mm\\:ss}, " +
$"Service {customer.ServiceTime:mm\\:ss}");
}
}
Console.WriteLine();
eventLogger.LogStatistics("Simulation end time", $"{result.SimulationEndTime:HH:mm}");
Console.WriteLine();
// Optional: Detailed report
if (ConsoleExtensions.ReadString("Show detailed report? (y/n)", "n").ToLower() == "y")
{
eventLogger.LogSectionHeader("DETAILED REPORT");
Console.WriteLine("Served Customers:");
foreach (var customer in result.ServedCustomers)
{
Console.WriteLine($" #{customer.Id:000} {customer.Gender,-6} " +
$"Arr: {customer.ArrivalTime:HH:mm} " +
$"Start: {customer.ServiceStartTime:HH:mm} " +
$"End: {customer.ServiceEndTime:HH:mm} " +
$"Wait: {customer.WaitTime:mm\\:ss} " +
$"Service: {customer.ServiceTime:mm\\:ss}");
}
if (result.LostCustomers.Count > 0)
{
Console.WriteLine("\nLost Customers:");
foreach (var customer in result.LostCustomers)
{
Console.WriteLine($" #{customer.Id:000} {customer.Gender} arrived at {customer.ArrivalTime:HH:mm}");
}
}
}
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
Step 5: Add Dependency Injection (Optional Enhancement)
Create DependencyInjection.cs:
csharp
using Microsoft.Extensions.DependencyInjection;
using BarberShopSimulation.Core.Services;
using BarberShopSimulation.Infrastructure;
using BarberShopSimulation.Simulation;
namespace BarberShopSimulation
{
public static class DependencyInjection
{
public static IServiceCollection ConfigureServices()
{
var services = new ServiceCollection();
// Infrastructure
services.AddSingleton<IRandomNumberGenerator, RandomNumberGenerator>();
services.AddSingleton<ITimeProvider, SystemTimeProvider>();
// Core Services
services.AddScoped<ISimulationService, SimulationService>();
// Simulation
services.AddScoped<EventLogger>();
return services;
}
}
}
Step 6: Build and Run
bash
# Restore packages
dotnet restore
# Build the project
dotnet build
# Run the simulation
dotnet run --project BarberShopSimulation/BarberShopSimulation.csproj
Best Practices Implemented:
-
Separation of Concerns: Clear separation between models, services, infrastructure
-
Dependency Injection: Loose coupling between components
-
Interfaces: Abstraction for testability
-
Configuration Management: appsettings.json for configurable parameters
-
Error Handling: Type-safe parameter validation
-
Single Responsibility: Each class has one clear purpose
-
Testability: Mockable interfaces for unit testing
-
Extensibility: Easy to add new features or modify existing ones
This structure provides a professional, maintainable, and extensible implementation that follows .NET best practices.
Sample Output Structure:
text
=======================================
BARBER SHOP SIMULATION
=======================================
Shop hours: 09:00 - 18:00
Waiting chairs: 4
=======================================
[09:00] Barber opens the shop.
[09:22] Customer #1 (Male) arrives.
→ Barber is free. Starting haircut immediately.
→ Starting Male's haircut. Expected finish: 09:37 (duration: 15 minutes)
[09:37] Finished haircut for Customer #1 (Male). Service time: 15:00
[09:48] Customer #2 (Female) arrives.
→ Barber is free. Starting haircut immediately.
→ Starting Female's haircut. Expected finish: 10:33 (duration: 45 minutes)
... (more events)
[18:15] Finished haircut for last customer.
=======================================
SIMULATION RESULTS
=======================================
Total customers arrived: 25
Total customers served: 22
Total customers lost: 3
Average wait time: 12:34
Average service time: 32:15
=======================================
The simulation provides a complete day-in-the-life view of the barber shop with all required events logged and statistics calculated.
Comments (0)
No comments yet. Be the first to comment!