Files
elwig/Elwig/Helpers/Utils.cs

572 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.IO.Ports;
using System.Net.Sockets;
using Elwig.Dialogs;
using System.Text;
using System.Numerics;
using Elwig.Models.Entities;
using Elwig.Helpers.Billing;
using System.Runtime.InteropServices;
using System.Net.Http;
using System.Text.Json.Nodes;
using System.IO;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
using System.Collections;
using Elwig.Documents;
using MimeKit;
using System.Windows.Input;
using LinqKit;
using System.Linq.Expressions;
using Elwig.Models;
using Microsoft.Win32;
namespace Elwig.Helpers {
public static partial class Utils {
public static readonly Encoding UTF8 = new UTF8Encoding(false, true);
public static int CurrentYear => DateTime.Now.Year;
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 6 ? 1 : 0);
public static int FollowingSeason => DateTime.Now.Year + (DateTime.Now.Month >= 11 ? 1 : 0);
public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1);
[GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
private static partial Regex GeneratedSerialRegex();
public static readonly Regex SerialRegex = GeneratedSerialRegex();
[GeneratedRegex("^tcp://([A-Za-z0-9._-]+):([0-9]+)$", RegexOptions.Compiled)]
private static partial Regex GeneratedTcpRegex();
public static readonly Regex TcpRegex = GeneratedTcpRegex();
[GeneratedRegex(@"^(-?(0?[1-9]|[12][0-9]|3[01])\.(0?[1-9]|1[0-2])\.([0-9]{4})?-?){1,2}$", RegexOptions.Compiled)]
private static partial Regex GeneratedFromToDateRegex();
public static readonly Regex DateFromToRegex = GeneratedFromToDateRegex();
[GeneratedRegex(@"^([0-9]+([\.,][0-9]+)?)?-([0-9]+([\.,][0-9]+)?)?$", RegexOptions.Compiled)]
private static partial Regex GeneratedFromToRegex();
public static readonly Regex FromToRegex = GeneratedFromToRegex();
[GeneratedRegex(@"^([0-9]{1,2}:[0-9]{2})?-([0-9]{1,2}:[0-9]{2})?$", RegexOptions.Compiled)]
private static partial Regex GeneratedFromToTimeRegex();
public static readonly Regex FromToTimeRegex = GeneratedFromToTimeRegex();
[GeneratedRegex(@"^(.*?) +([0-9].*)$", RegexOptions.Compiled)]
private static partial Regex GeneratedAddressRegex();
public static readonly Regex AddressRegex = GeneratedAddressRegex();
[GeneratedRegex(@"[^A-Za-z0-9ÄÜÖäöüß-]+")]
private static partial Regex GeneratedInvalidFileNamePartsRegex();
public static readonly Regex InvalidFileNamePartsRegex = GeneratedInvalidFileNamePartsRegex();
public static readonly string GroupSeparator = "\u202F";
public static readonly string UnitSeparator = "\u00A0";
public static readonly KeyValuePair<string, string>[] PhoneNrTypes = [
new("landline", "Tel.-Nr. (Festnetz)"),
new("mobile", "Tel.-Nr. (mobil)"),
new("fax", "Fax-Nr."),
];
public static string PhoneNrTypeToString(string type) {
return PhoneNrTypes.Where(t => t.Key == type).Select(t => t.Value).FirstOrDefault(type);
}
private static readonly string[] TempWildcards = ["*.html", "*.pdf", "*.exe", "*.zip"];
private static readonly ushort[] Crc16ModbusTable = [
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
];
public static SerialPort OpenSerialConnection(string connection) {
var m = SerialRegex.Match(connection);
if (!m.Success)
throw new ArgumentException("Invalid connection string for scheme \"serial\"");
var stop = m.Groups[6].Value;
var parity = m.Groups[5].Value.ToUpper();
var data = m.Groups[4].Value;
var port = new SerialPort() {
PortName = m.Groups[1].Value,
BaudRate = int.Parse(m.Groups[2].Value),
Parity = parity == "E" ? Parity.Even :
parity == "O" ? Parity.Odd :
parity == "M" ? Parity.Mark :
parity == "S" ? Parity.Space :
Parity.None,
DataBits = data == "" ? 8 : int.Parse(data),
StopBits = (StopBits)(stop == "" ? 1 : stop == "1.5" ? 3 : stop[0] - '0'),
Handshake = Handshake.None,
WriteTimeout = 250,
ReadTimeout = 11000,
};
port.Open();
return port;
}
public static TcpClient OpenTcpConnection(string connection) {
var m = TcpRegex.Match(connection);
if (!m.Success)
throw new ArgumentException("Invalid connection string for scheme \"tcp\"");
var client = new TcpClient() {
SendTimeout = 250,
ReceiveTimeout = 11000,
};
client.Connect(m.Groups[1].Value, int.Parse(m.Groups[2].Value));
return client;
}
public static int Modulo(string a, int b) {
if (a.Length == 0 || !a.All(char.IsAsciiDigit)) {
throw new ArgumentException("First argument has to be a decimal string");
} else if (b < 2) {
throw new ArgumentException("Second argument has to be greater than 1");
}
return a.Select(ch => ch - '0').Aggregate((sum, n) => (sum * 10 + n) % b);
}
public static ushort CalcCrc16Modbus(byte[] data) {
// https://www.modbustools.com/modbus_crc16.htm
byte temp;
ushort crc = 0xFFFF;
foreach (byte b in data) {
temp = (byte)(b ^ crc);
crc >>= 8;
crc ^= Crc16ModbusTable[temp];
}
return crc;
}
public static ushort CalcCrc16Modbus(string data) {
return CalcCrc16Modbus(Encoding.ASCII.GetBytes(data));
}
public static string FormatIban(string iban) {
return Regex.Replace(iban.Trim(), ".{4}", "$0 ").Trim();
}
public static void RunBackground(string title, Func<Task> a) {
Task.Run(async () => {
try {
await a();
} catch (Exception e) {
MessageBox.Show(e.ToString(), title, MessageBoxButton.OK, MessageBoxImage.Error);
}
});
}
public static void MailTo(string emailAddress) {
MailTo(new string[] { emailAddress });
}
public static void MailTo(IEnumerable<string> emailAddresses) {
Process.Start(new ProcessStartInfo() {
FileName = $"mailto:{string.Join(",%20", emailAddresses)}",
UseShellExecute = true,
});
}
public static double KmwToOe(double kmw) {
return Math.Round(kmw * (4.54 + 0.022 * kmw), 0);
}
public static double OeToKmw(double oe) {
return Math.Round((-4.54 + Math.Sqrt(4.54 * 4.54 - 4 * 0.022 * -oe)) / (2 * 0.022), 1);
}
public static decimal DecFromDb(long value, byte precision) {
bool neg = value < 0;
if (neg) value = -value;
return new((int)(value & 0xFFFFFFFF), (int)((value >> 32) & 0x7FFFFFFF), 0, neg, precision);
}
public static long DecToDb(decimal value, byte precision) {
return (long)decimal.Round(value * (decimal)Math.Pow(10, precision), 0);
}
public static int GetAge(DateOnly birthday) {
var today = DateTime.Today;
var a = (today.Year * 100 + today.Month) * 100 + today.Day;
var b = (birthday.Year * 100 + birthday.Month) * 100 + birthday.Day;
return (a - b) / 10000;
}
public static int GetSearchScore(IEnumerable<string?> words, IEnumerable<string> searchKeywords) {
searchKeywords = searchKeywords.Where(s => s.Length >= 2 || (s.Length > 0 && s.All(c => char.IsAsciiDigit(c))));
if (!searchKeywords.Any())
return 0;
return searchKeywords
.Select(k => {
k = k.ToLower();
var scores = words.Select(w => {
w = w?.ToLower();
var p = w?.ToLower()?.Split(" ");
if (w == null || p == null) {
return 0;
} else if (k == w) {
return 4 + k.Length;
} else if (p.Any(a => a == k)) {
return 3 + k.Length;
} else if (p.Any(a => a.StartsWith(k))) {
return 2 + k.Length;
} else if (w.Contains(k)) {
return 1 + k.Length;
} else {
return 0;
}
});
return scores.Max() + scores.Count(s => s > 0);
})
.Sum();
}
public static (int, string?)? ShowManualWeighingDialog(string? reason = null) {
var d = new ManualWeighingDialog(reason);
return d.ShowDialog() == true ? (d.Weight, d.Reason) : null;
}
public static int? ShowAbwertenDialog(string lsnr, string name, int weight) {
var d = new AbwertenDialog(lsnr, name, weight);
return d.ShowDialog() == true ? d.Weight : null;
}
public static double? ShowLinearPriceIncreaseDialog() {
var d = new LinearPriceIncreaseDialog();
return d.ShowDialog() == true ? d.Price : null;
}
public static string? ShowDeliveryExtractionDialog(string lsnr, string name, bool single, IEnumerable<string> lsnrs) {
var d = new DeliveryExtractionDialog(lsnr, name, single, lsnrs);
return d.ShowDialog() == true ? d.AddTo : null;
}
public static Footer GenerateFooter(string lineBreak, string seperator) {
return new Footer(lineBreak, seperator);
}
public class Footer {
private string Text = "";
private readonly string LineBreak;
private readonly string Seperator;
private bool FirstLine = true;
private bool FirstItemInLine = true;
public Footer(string lineBreak, string seperator) {
LineBreak = lineBreak;
Seperator = seperator;
}
public Footer Item(string? text) {
if (text == null) return this;
Text += FirstItemInLine ? (FirstLine ? "" : LineBreak) : Seperator;
Text += text;
FirstLine = false;
FirstItemInLine = false;
return this;
}
public Footer Item(string name, string? text) {
return text == null ? this : Item($"{name}: {text}");
}
public Footer NextLine() {
FirstItemInLine = true;
return this;
}
public override string ToString() {
return Text;
}
}
public static string GetSign<T>(T number) where T : INumber<T>
=> T.Sign(number) switch {
< 0 => "\u2212", // minus
0 => "\u00b1", // plus minus
> 0 => "+",
};
public static double AggregateDeliveryPartsKmw(IEnumerable<IDelivery> parts)
=> parts.Aggregate(
(Weight: 0, Kmw: 0.0),
(sum, item) => (
sum.Weight + item.Weight,
(sum.Kmw * sum.Weight + item.Kmw * item.Weight) / (sum.Weight + item.Weight)
),
sum => sum.Kmw
);
public static string GenerateLsNr(Delivery d) => GenerateLsNr(d.Date, d.ZwstId, d.LNr);
public static string GenerateLsNr(DateOnly date, string zwstid, int lnr) => $"{date:yyyyMMdd}{zwstid}{lnr:000}";
public static (string, string?) SplitAddress(string address) {
var m = AddressRegex.Match(address);
return (m.Groups[1].Value, m.Groups[2].Value);
}
public static (string, string?) SplitName(string fullName, string? familyName) {
if (familyName == null || familyName == "") return (fullName, null);
var p0 = fullName.IndexOf(familyName, StringComparison.CurrentCultureIgnoreCase);
if (p0 == -1) return (fullName, null);
var p1 = fullName.IndexOf(" und ");
var p2 = fullName.ToLower().LastIndexOf(" und ");
if (p1 != p2) {
if (p0 > p1) {
// A und B familyName [und ...]
return (fullName[p0..^0], fullName[0..(p0 - 1)]);
} else {
// familyName und ... A und B
var p3 = fullName.LastIndexOf(' ', p2 - 1);
return (fullName[0..p3], fullName[(p3 + 1)..^0]);
}
} else {
return (familyName, fullName.Replace(familyName, "").Replace(" ", " ").Trim());
}
}
public static IEnumerable<IEnumerable<T>> Permutate<T>(IEnumerable<T> input, IEnumerable<T>? forced = null) {
HashSet<IEnumerable<T>> output = [];
for (int i = 0; i < Math.Pow(2, input.Count()); i++) {
List<T> t = [];
for (int j = 0; j < 30; j++) {
var e = input.ElementAtOrDefault(j);
if (e != null && ((forced?.Contains(e) ?? false) || (i & (1 << j)) != 0)) {
t.Add(e);
}
}
output.Add(t);
}
return output.OrderByDescending(l => l.Count());
}
public static List<RawVaribute> GetVaributes(AppDbContext ctx, int year, bool onlyDelivered = true) {
var varieties = ctx.WineVarieties.Select(v => new RawVaribute(v.SortId, "", null)).ToList();
var delivered = ctx.DeliveryParts
.Where(d => d.Year == year)
.Select(d => new RawVaribute(d.SortId, d.AttrId ?? "", d.CultId ?? ""))
.Distinct()
.ToList();
return [.. (onlyDelivered ? delivered : delivered.Union(varieties)).Order()];
}
public static List<Varibute> GetVaributeList(AppDbContext ctx, int year, bool onlyDelivered = true) {
var varieties = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v);
var attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a);
var cultivations = ctx.WineCultivations.ToDictionary(c => c.CultId, c => c);
return GetVaributes(ctx, year, onlyDelivered)
.Select(s => new Varibute(s, varieties, attributes, cultivations))
.ToList();
}
[LibraryImport("wininet.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool InternetGetConnectedState(out int description, int reservedValue);
public static bool HasInternetConnectivity() {
return InternetGetConnectedState(out var _, 0);
}
public static HttpClient GetHttpClient(string? username = null, string? password = null, string? accept = null) {
var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5),
};
client.DefaultRequestHeaders.Accept.Clear();
if (accept != null)
client.DefaultRequestHeaders.Accept.Add(new(accept));
if (username != null || password != null)
client.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(
Utils.UTF8.GetBytes($"{username}:{password}")));
return client;
}
public static async Task<(string Version, string Url, long Size)?> GetLatestInstallerUrl(string url) {
try {
using var client = GetHttpClient(accept: "application/json");
using var res = await client.GetAsync(url);
if (!res.IsSuccessStatusCode)
return null;
var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
var data = resJson!["data"]!.AsArray()[^1]!;
return ((string)data["version"]!, (string)data["url"]!, (long)data["size"]!);
} catch {
return null;
}
}
public static async Task UploadExportData(string zip, string url, string username, string password) {
if (!url.EndsWith('/')) url += "/";
using var client = GetHttpClient(username, password, accept: "application/json");
var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
content.Headers.ContentType = new("application/zip");
using var res = await client.PutAsync(url + Path.GetFileName(zip), content);
res.EnsureSuccessStatusCode();
}
public static async Task<JsonArray> GetExportMetaData(string url, string username, string password) {
using var client = GetHttpClient(username, password, accept: "application/json");
using var res = await client.GetAsync(url);
res.EnsureSuccessStatusCode();
var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
var data = resJson!["data"]!;
return data.AsArray();
}
public static void CleanupTempFiles() {
var dir = new DirectoryInfo(App.TempPath);
foreach (var file in TempWildcards.SelectMany(dir.EnumerateFiles)) {
file.Delete();
}
}
public static string NormalizeFileName(string filename) {
return InvalidFileNamePartsRegex.Replace(filename.Replace('/', '-'), "_");
}
public static async Task<SmtpClient?> GetSmtpClient() {
if (App.Config.Smtp == null)
return null;
var (host, port, mode, username, password, _) = App.Config.Smtp.Value;
var client = new SmtpClient();
await client.ConnectAsync(host, port, mode == "starttls" ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
await client.AuthenticateAsync(username, password);
return client;
}
public static async Task<bool> SendEmail(Member member, string subject, string text, IEnumerable<Document> docs) {
if (App.Config.Smtp == null)
return false;
SmtpClient? client = null;
try {
Mouse.OverrideCursor = Cursors.AppStarting;
client = await GetSmtpClient();
using var msg = new MimeMessage();
msg.From.Add(new MailboxAddress(App.Client.NameFull, App.Config.Smtp.Value.From));
msg.To.AddRange(member.EmailAddresses.OrderBy(a => a.Nr).Select(a => new MailboxAddress(member.AdministrativeName, a.Address)));
msg.Subject = subject;
var body = new Multipart("mixed") {
new TextPart("plain") { Text = text }
};
foreach (var doc in docs) {
var name = NormalizeFileName(doc.Title);
body.Add(doc.AsEmailAttachment($"{name}.pdf"));
}
msg.Body = body;
await client!.SendAsync(msg);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
} finally {
if (client != null)
await client.DisconnectAsync(true);
client?.Dispose();
Mouse.OverrideCursor = null;
}
return true;
}
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member, string, string)? emailData = null) {
if (mode == ExportMode.Print && !App.Config.Debug) {
await doc.Generate();
await doc.Print();
} else if (mode == ExportMode.Email && emailData is (Member, string, string) e) {
await doc.Generate();
var success = await SendEmail(e.Item1, e.Item2, e.Item3, [doc]);
if (success)
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!", "E-Mail verschickt",
MessageBoxButton.OK, MessageBoxImage.Information);
} else if (mode == ExportMode.SavePdf) {
var d = new SaveFileDialog() {
FileName = $"{NormalizeFileName(filename ?? doc.Title)}.pdf",
DefaultExt = "pdf",
Filter = "PDF-Datei (*.pdf)|*.pdf",
Title = $"{doc.Title} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
await doc.Generate();
doc.SaveTo(d.FileName);
Process.Start("explorer.exe", d.FileName);
}
} else {
await doc.Generate();
doc.Show();
}
}
public static int GetEntityIdetifierForPk(params object?[] primaryKey) {
var pk = primaryKey.Select(k => k?.GetHashCode() ?? 0).ToArray();
return ((IStructuralEquatable)pk).GetHashCode(EqualityComparer<int>.Default);
}
public static int? GetEntityIdentifier(object? obj) {
if (obj == null) {
return null;
} else if (obj is IEnumerable list && obj is not string) {
var arr = list.Cast<object>().Select(GetEntityIdentifier).ToArray();
return ((IStructuralEquatable)arr).GetHashCode(EqualityComparer<int?>.Default);
} else if (obj.GetType().GetCustomAttribute(typeof(PrimaryKeyAttribute), true) is PrimaryKeyAttribute pkAttr) {
var pk = pkAttr.PropertyNames.Select(name => obj.GetType().GetProperty(name)!.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
return ((IStructuralEquatable)pk).GetHashCode(EqualityComparer<int>.Default);
} else {
return obj.GetHashCode();
}
}
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear);
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments(int year) =>
c => (c.YearFrom <= year) && (c.YearTo == null || c.YearTo >= year);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query, int year) =>
query.Where(ActiveAreaCommitments(year));
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query, int year) =>
query.Where(c => ActiveAreaCommitments(year).Invoke(c));
}
}