From bbe25192723864941a00d6a32317d704a7e9be10 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sun, 12 May 2024 16:22:58 -0400 Subject: [PATCH 1/2] Implement a version of ShadowTheAge#181, allowing XXb to specify XX belts of production. Also look for /s, /m, or /h suffixes. If present, use that suffix regardless of the current display settings. --- Yafc.Model.Tests/Data/DataUtils.cs | 414 +++++++++++++++++++++++++++++ Yafc.Model/Data/DataUtils.cs | 136 +++++++--- Yafc/Data/Tips.txt | 5 +- changelog.txt | 3 + 4 files changed, 514 insertions(+), 44 deletions(-) create mode 100644 Yafc.Model.Tests/Data/DataUtils.cs diff --git a/Yafc.Model.Tests/Data/DataUtils.cs b/Yafc.Model.Tests/Data/DataUtils.cs new file mode 100644 index 00000000..48ba9d02 --- /dev/null +++ b/Yafc.Model.Tests/Data/DataUtils.cs @@ -0,0 +1,414 @@ +using System; +using Xunit; + +namespace Yafc.Model.Data.Tests; + +public class DataUtilsTests { + public DataUtilsTests() => Project.current = new(); + + [Fact] + public void TryParseAmount_IsInverseOfFormatValue() { + // Hammer the formatter and parser with lots of random but repeatable values, making sure TryParseAmount can correctly read anything FormatAmount generates. + Random r = new Random(0); + byte[] bytes = new byte[4]; + for (int i = 0; i < 1000; i++) { + for (UnitOfMeasure unit = 0; unit < UnitOfMeasure.Celsius; unit++) { + float value; + int count = 1; + do { + r.NextBytes(bytes); + value = BitConverter.ToSingle(bytes, 0); + count++; + // TryParseAmount refuses values above 1e15, and FormatAmount has large relative errors for tiny values. But 1e-7 is 1 item every 116 days, so we can ignore that. + } while (MathF.Abs(value) is < 1e-7f or > 9.9e14f || float.IsNaN(value)); + + Project.current.preferences.time = r.Next(4) switch { + 0 => 1, + 1 => 60, + 2 => 3600, + 3 => r.Next(100000), + _ => throw new Exception("r.Next returned an out of range value.") // Can't happen, but it suppresses the warning. + }; + + string formattedValue = DataUtils.FormatAmount(value, unit, precise: true); + Assert.True(DataUtils.TryParseAmount(formattedValue, out float parsedValue, unit), $"Could not parse {value} after being formatted precisely as {formattedValue}."); + Assert.True(Math.Abs(value - parsedValue) <= Math.Abs(value * .00001), $"Incorrectly parsed {value}, formatted precisely as {formattedValue}, to {parsedValue}."); + + formattedValue = DataUtils.FormatAmount(value, unit); + Assert.True(DataUtils.TryParseAmount(formattedValue, out parsedValue, unit), $"Could not parse {value} after being formatted as {formattedValue}."); + // Even within the allowed range, imprecise formatting is extra imprecise when the value is just over a power of 10; 0.0010209 is formatted as "0.001" + // This can't exceed a 5% error: 0.00104999 would also round down to 0.001, but .00105 rounds up. + Assert.True(Math.Abs(value - parsedValue) <= Math.Abs(value * .05), $"Incorrectly parsed {value}, formatted as {formattedValue}, to {parsedValue}."); + } + } + } + + [Fact] + public void TryParseAmount_IsInverseOfFormatValue_WithBeltsAndPipes() { + // Hammer the formatter and parser with lots of random but repeatable values, making sure TryParseAmount can correctly read anything FormatAmount generates. + // This time, include b and p suffixes. These suffixes noticably reduce precision, so do them separately. + Random r = new Random(0); + byte[] bytes = new byte[4]; + for (int i = 0; i < 1000; i++) { + for (UnitOfMeasure unit = 0; unit < UnitOfMeasure.Celsius; unit++) { + float value; + int count = 1; + do { + r.NextBytes(bytes); + value = BitConverter.ToSingle(bytes, 0); + count++; + // TryParseAmount refuses values above 1e15, and FormatAmount has large relative errors for tiny values. But 1e-7 is 1 item every 116 days, so we can ignore that. + } while (MathF.Abs(value) is < 1e-7f or > 9.9e14f || float.IsNaN(value)); + + Project.current.preferences.itemUnit = r.Next(6) switch { + 0 or 1 => 0, + int x => (x - 1) * 15 + }; + Project.current.preferences.fluidUnit = r.Next(6) switch { + 0 or 1 => 0, + int x => (x - 1) * 60 + }; + + string formattedValue = DataUtils.FormatAmount(value, unit, precise: true); + Assert.True(DataUtils.TryParseAmount(formattedValue, out float parsedValue, unit), $"Could not parse {value} after being formatted precisely as {formattedValue}."); + // Precise formatting loses a lot of precision when formatting 'N belts' or 'N pipes'. + Assert.True(Math.Abs(value - parsedValue) <= Math.Abs(value * .001), $"Incorrectly parsed {value}, formatted precisely as {formattedValue}, to {parsedValue}."); + + formattedValue = DataUtils.FormatAmount(value, unit); + // Skip testing if the formatted value is less than 0.1μ; we've lost too much precision for this to be meaningful. + if (((!formattedValue.StartsWith("-0.0") && !formattedValue.StartsWith("0.0")) || !formattedValue.Contains('μ')) && !formattedValue.StartsWith("0μ") && !formattedValue.StartsWith("-0μ")) { + Assert.True(DataUtils.TryParseAmount(formattedValue, out parsedValue, unit), $"Could not parse {value} after being formatted as {formattedValue}."); + // Allow slightly more rounding error when parsing imprecise belt and pipe counts. + Assert.True(Math.Abs(value - parsedValue) <= Math.Abs(value * .06), $"Incorrectly parsed {value}, formatted as {formattedValue}, to {parsedValue}."); + } + } + } + } + + [Theory] + [MemberData(nameof(TryParseAmount_TestData))] + public void TryParseAmount_WhenGivenInputs_ShouldProduceCorrectValues(string input, UnitOfMeasure unitOfMeasure, bool expectedReturn, float expectedOutput, int time, int itemUnit, int fluidUnit, int defaultBeltSpeed) { + Project.current.preferences.time = time; + Project.current.preferences.itemUnit = itemUnit; + Project.current.preferences.fluidUnit = fluidUnit; + Project.current.preferences.defaultBelt ??= new(); + typeof(EntityBelt).GetProperty(nameof(EntityBelt.beltItemsPerSecond)).SetValue(Project.current.preferences.defaultBelt, defaultBeltSpeed); + + Assert.Equal(expectedReturn, DataUtils.TryParseAmount(input, out float result, unitOfMeasure)); + if (expectedReturn) { + double error = (result - expectedOutput) / (double)expectedOutput; + Assert.True(Math.Abs(error) < .00001, $"Parsing {input} produced {result}, which differs from the expected {expectedOutput} by {error:0.00%}."); + } + } + + public static object[][] TryParseAmount_TestData => [ + new DataItem("1", UnitOfMeasure.None, true, 1), + new DataItem("10e-2k", UnitOfMeasure.None, true, 100), + new DataItem("-10e-2k", UnitOfMeasure.None, true, -100), + new DataItem(".1e2u", UnitOfMeasure.None, true, .00001f), + new DataItem("10%", UnitOfMeasure.None, false), + new DataItem("10j", UnitOfMeasure.None, false), + new DataItem("10 M", UnitOfMeasure.None, true, 10000000), + new DataItem("10/t", UnitOfMeasure.None, false), + new DataItem("10u/s", UnitOfMeasure.None, false), + new DataItem("10m/m", UnitOfMeasure.None, false), + new DataItem("10g/h", UnitOfMeasure.None, false), + new DataItem("10/ks", UnitOfMeasure.None, false), + new DataItem("10 s", UnitOfMeasure.None, false), + new DataItem("10 b", UnitOfMeasure.None, false), + new DataItem("10 p", UnitOfMeasure.None, false), + + new DataItem("1", UnitOfMeasure.Percent, true, .01f), + new DataItem("10e-2k", UnitOfMeasure.Percent, true, 1), + new DataItem("-10e-2k", UnitOfMeasure.Percent, true, -1), + new DataItem(".1e2u", UnitOfMeasure.Percent, true, .0000001f), + new DataItem("10%", UnitOfMeasure.Percent, true, .1f), + new DataItem("10j", UnitOfMeasure.Percent, false), + new DataItem("10 M", UnitOfMeasure.Percent, true, 100000), + new DataItem("10/t", UnitOfMeasure.Percent, false), + new DataItem("10u/s", UnitOfMeasure.Percent, false), + new DataItem("10m/m", UnitOfMeasure.Percent, false), + new DataItem("10g/h", UnitOfMeasure.Percent, false), + new DataItem("10/ks", UnitOfMeasure.Percent, false), + new DataItem("10 s", UnitOfMeasure.Percent, false), + new DataItem("10 b", UnitOfMeasure.Percent, false), + new DataItem("10 p", UnitOfMeasure.Percent, false), + + new DataItem("1", UnitOfMeasure.Second, true, 1), + new DataItem("10e-2k", UnitOfMeasure.Second, true, 100), + new DataItem("-10e-2k", UnitOfMeasure.Second, true, -100), + new DataItem(".1e2u", UnitOfMeasure.Second, true, .00001f), + new DataItem("10%", UnitOfMeasure.Second, false), + new DataItem("10j", UnitOfMeasure.Second, false), + new DataItem("10 M", UnitOfMeasure.Second, true, 10000000), + new DataItem("10/t", UnitOfMeasure.Second, false), + new DataItem("10u/s", UnitOfMeasure.Second, false), + new DataItem("10m/m", UnitOfMeasure.Second, false), + new DataItem("10g/h", UnitOfMeasure.Second, false), + new DataItem("10/ks", UnitOfMeasure.Second, false), + new DataItem("10 s", UnitOfMeasure.Second, true, 10), + new DataItem("10 b", UnitOfMeasure.Second, false), + new DataItem("10 p", UnitOfMeasure.Second, false), + + new DataItem("1", UnitOfMeasure.PerSecond, true, 1), + new DataItem("10e-2k", UnitOfMeasure.PerSecond, true, 100), + new DataItem("-10e-2k", UnitOfMeasure.PerSecond, true, -100), + new DataItem(".1e2u", UnitOfMeasure.PerSecond, true, .00001f), + new DataItem("10%", UnitOfMeasure.PerSecond, false), + new DataItem("10j", UnitOfMeasure.PerSecond, false), + new DataItem("10 M", UnitOfMeasure.PerSecond, true, 10000000), + new DataItem("10/t", UnitOfMeasure.PerSecond, true, 10), + new DataItem("10u/s", UnitOfMeasure.PerSecond, true, .00001f), + new DataItem("10m/m", UnitOfMeasure.PerSecond, true, 166666.6666666667f), + new DataItem("10g/h", UnitOfMeasure.PerSecond, true, 2777777.777777778f), + new DataItem("10/ks", UnitOfMeasure.PerSecond, false), + new DataItem("10 s", UnitOfMeasure.PerSecond, false), + new DataItem("10 b", UnitOfMeasure.PerSecond, false), + new DataItem("10 p", UnitOfMeasure.PerSecond, false), + + new DataItem("1", UnitOfMeasure.PerSecond, true, 1f/30, time: 30), + new DataItem("10e-2k", UnitOfMeasure.PerSecond, true, 100f/30, time: 30), + new DataItem("-10e-2k", UnitOfMeasure.PerSecond, true, -100f/30, time: 30), + new DataItem(".1e2u", UnitOfMeasure.PerSecond, true, .00001f/30, time: 30), + new DataItem("10%", UnitOfMeasure.PerSecond, false, time: 30), + new DataItem("10j", UnitOfMeasure.PerSecond, false, time: 30), + new DataItem("10 M", UnitOfMeasure.PerSecond, true, 10000000f/30, time: 30), + new DataItem("10/t", UnitOfMeasure.PerSecond, true, 10f/30, time: 30), + new DataItem("10u/s", UnitOfMeasure.PerSecond, true, .00001f, time: 30), + new DataItem("10m/m", UnitOfMeasure.PerSecond, true, 166666.6666666667f, time: 30), + new DataItem("10g/h", UnitOfMeasure.PerSecond, true, 2777777.777777778f, time: 30), + new DataItem("10/ks", UnitOfMeasure.PerSecond, false, time: 30), + new DataItem("10 s", UnitOfMeasure.PerSecond, false, time: 30), + new DataItem("10 b", UnitOfMeasure.PerSecond, false, time: 30), + new DataItem("10 p", UnitOfMeasure.PerSecond, false, time: 30), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 1), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 100), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -100), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00001f), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 10000000), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 150), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 1f/30, time: 30), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 100f/30, time: 30), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -100f/30, time: 30), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00001f/30, time: 30), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, time: 30), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, time: 30), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 10000000f/30, time: 30), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10f/30, time: 30), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, time: 30), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, time: 30), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, time: 30), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, time: 30), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, time: 30), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 150, time: 30), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, time: 30), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 1, defaultBeltSpeed: 45), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 100, defaultBeltSpeed: 45), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -100, defaultBeltSpeed: 45), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00001f, defaultBeltSpeed: 45), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 10000000, defaultBeltSpeed: 45), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10, defaultBeltSpeed: 45), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, defaultBeltSpeed: 45), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, defaultBeltSpeed: 45), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, defaultBeltSpeed: 45), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 450, defaultBeltSpeed: 45), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 1f/30, time: 30, defaultBeltSpeed: 45), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 100f/30, time: 30, defaultBeltSpeed: 45), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -100f/30, time: 30, defaultBeltSpeed: 45), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00001f/30, time: 30, defaultBeltSpeed: 45), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 10000000f/30, time: 30, defaultBeltSpeed: 45), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10f/30, time: 30, defaultBeltSpeed: 45), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, time: 30, defaultBeltSpeed: 45), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, time: 30, defaultBeltSpeed: 45), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, time: 30, defaultBeltSpeed: 45), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 450, time: 30, defaultBeltSpeed: 45), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 22, itemUnit: 22), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 2200, itemUnit: 22), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -2200, itemUnit: 22), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00022f, itemUnit: 22), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, itemUnit: 22), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, itemUnit: 22), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 220000000, itemUnit: 22), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10, itemUnit: 22), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, itemUnit: 22), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, itemUnit: 22), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, itemUnit: 22), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, itemUnit: 22), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, itemUnit: 22), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 220, itemUnit: 22), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, itemUnit: 22), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 22, time: 30, itemUnit: 22), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 2200, time: 30, itemUnit: 22), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -2200, time: 30, itemUnit: 22), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00022f, time: 30, itemUnit: 22), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, time: 30, itemUnit: 22), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, time: 30, itemUnit: 22), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 220000000f, time: 30, itemUnit: 22), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10f/30, time: 30, itemUnit: 22), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, time: 30, itemUnit: 22), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, time: 30, itemUnit: 22), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, time: 30, itemUnit: 22), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, time: 30, itemUnit: 22), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, time: 30, itemUnit: 22), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 220, time: 30, itemUnit: 22), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, time: 30, itemUnit: 22), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 22, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 2200, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -2200, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00022f, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 220000000, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 220, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, defaultBeltSpeed: 45, itemUnit: 22), + + new DataItem("1", UnitOfMeasure.ItemPerSecond, true, 22, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10e-2k", UnitOfMeasure.ItemPerSecond, true, 2200, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("-10e-2k", UnitOfMeasure.ItemPerSecond, true, -2200, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem(".1e2u", UnitOfMeasure.ItemPerSecond, true, .00022f, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10%", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10j", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 M", UnitOfMeasure.ItemPerSecond, true, 220000000f, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10/t", UnitOfMeasure.ItemPerSecond, true, 10f/30, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10u/s", UnitOfMeasure.ItemPerSecond, true, .00001f, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10m/m", UnitOfMeasure.ItemPerSecond, true, 166666.6666666667f, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10g/h", UnitOfMeasure.ItemPerSecond, true, 2777777.777777778f, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10/ks", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 s", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 b", UnitOfMeasure.ItemPerSecond, true, 220, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + new DataItem("10 p", UnitOfMeasure.ItemPerSecond, false, time: 30, defaultBeltSpeed: 45, itemUnit: 22), + + new DataItem("1", UnitOfMeasure.FluidPerSecond, true, 1), + new DataItem("10e-2k", UnitOfMeasure.FluidPerSecond, true, 100), + new DataItem("-10e-2k", UnitOfMeasure.FluidPerSecond, true, -100), + new DataItem(".1e2u", UnitOfMeasure.FluidPerSecond, true, .00001f), + new DataItem("10%", UnitOfMeasure.FluidPerSecond, false), + new DataItem("10j", UnitOfMeasure.FluidPerSecond, false), + new DataItem("10 M", UnitOfMeasure.FluidPerSecond, true, 10000000), + new DataItem("10/t", UnitOfMeasure.FluidPerSecond, true, 10), + new DataItem("10u/s", UnitOfMeasure.FluidPerSecond, true, .00001f), + new DataItem("10m/m", UnitOfMeasure.FluidPerSecond, true, 166666.6666666667f), + new DataItem("10g/h", UnitOfMeasure.FluidPerSecond, true, 2777777.777777778f), + new DataItem("10/ks", UnitOfMeasure.FluidPerSecond, false), + new DataItem("10 s", UnitOfMeasure.FluidPerSecond, false), + new DataItem("10 b", UnitOfMeasure.FluidPerSecond, false), + new DataItem("10 p", UnitOfMeasure.FluidPerSecond, false), + + new DataItem("1", UnitOfMeasure.FluidPerSecond, true, 1f/30, time: 30), + new DataItem("10e-2k", UnitOfMeasure.FluidPerSecond, true, 100f/30, time: 30), + new DataItem("-10e-2k", UnitOfMeasure.FluidPerSecond, true, -100f/30, time: 30), + new DataItem(".1e2u", UnitOfMeasure.FluidPerSecond, true, .00001f/30, time: 30), + new DataItem("10%", UnitOfMeasure.FluidPerSecond, false, time: 30), + new DataItem("10j", UnitOfMeasure.FluidPerSecond, false, time: 30), + new DataItem("10 M", UnitOfMeasure.FluidPerSecond, true, 10000000f/30, time: 30), + new DataItem("10/t", UnitOfMeasure.FluidPerSecond, true, 10f/30, time: 30), + new DataItem("10u/s", UnitOfMeasure.FluidPerSecond, true, .00001f, time: 30), + new DataItem("10m/m", UnitOfMeasure.FluidPerSecond, true, 166666.6666666667f, time: 30), + new DataItem("10g/h", UnitOfMeasure.FluidPerSecond, true, 2777777.777777778f, time: 30), + new DataItem("10/ks", UnitOfMeasure.FluidPerSecond, false, time: 30), + new DataItem("10 s", UnitOfMeasure.FluidPerSecond, false, time: 30), + new DataItem("10 b", UnitOfMeasure.FluidPerSecond, false, time: 30), + new DataItem("10 p", UnitOfMeasure.FluidPerSecond, false, time: 30), + + new DataItem("1", UnitOfMeasure.FluidPerSecond, true, 22, fluidUnit: 22), + new DataItem("10e-2k", UnitOfMeasure.FluidPerSecond, true, 2200, fluidUnit: 22), + new DataItem("-10e-2k", UnitOfMeasure.FluidPerSecond, true, -2200, fluidUnit: 22), + new DataItem(".1e2u", UnitOfMeasure.FluidPerSecond, true, .00022f, fluidUnit: 22), + new DataItem("10%", UnitOfMeasure.FluidPerSecond, false, fluidUnit: 22), + new DataItem("10j", UnitOfMeasure.FluidPerSecond, false, fluidUnit: 22), + new DataItem("10 M", UnitOfMeasure.FluidPerSecond, true, 220000000, fluidUnit: 22), + new DataItem("10/t", UnitOfMeasure.FluidPerSecond, true, 10, fluidUnit: 22), + new DataItem("10u/s", UnitOfMeasure.FluidPerSecond, true, .00001f, fluidUnit: 22), + new DataItem("10m/m", UnitOfMeasure.FluidPerSecond, true, 166666.6666666667f, fluidUnit: 22), + new DataItem("10g/h", UnitOfMeasure.FluidPerSecond, true, 2777777.777777778f, fluidUnit: 22), + new DataItem("10/ks", UnitOfMeasure.FluidPerSecond, false, fluidUnit: 22), + new DataItem("10 s", UnitOfMeasure.FluidPerSecond, false, fluidUnit: 22), + new DataItem("10 b", UnitOfMeasure.FluidPerSecond, false, fluidUnit: 22), + new DataItem("10 p", UnitOfMeasure.FluidPerSecond, true, 220, fluidUnit: 22), + + new DataItem("1", UnitOfMeasure.FluidPerSecond, true, 22, time: 30, fluidUnit: 22), + new DataItem("10e-2k", UnitOfMeasure.FluidPerSecond, true, 2200, time: 30, fluidUnit: 22), + new DataItem("-10e-2k", UnitOfMeasure.FluidPerSecond, true, -2200, time: 30, fluidUnit: 22), + new DataItem(".1e2u", UnitOfMeasure.FluidPerSecond, true, .00022f, time: 30, fluidUnit: 22), + new DataItem("10%", UnitOfMeasure.FluidPerSecond, false, time: 30, fluidUnit: 22), + new DataItem("10j", UnitOfMeasure.FluidPerSecond, false, time: 30, fluidUnit: 22), + new DataItem("10 M", UnitOfMeasure.FluidPerSecond, true, 220000000f, time: 30, fluidUnit: 22), + new DataItem("10/t", UnitOfMeasure.FluidPerSecond, true, 10f/30, time: 30, fluidUnit: 22), + new DataItem("10u/s", UnitOfMeasure.FluidPerSecond, true, .00001f, time: 30, fluidUnit: 22), + new DataItem("10m/m", UnitOfMeasure.FluidPerSecond, true, 166666.6666666667f, time: 30, fluidUnit: 22), + new DataItem("10g/h", UnitOfMeasure.FluidPerSecond, true, 2777777.777777778f, time: 30, fluidUnit: 22), + new DataItem("10/ks", UnitOfMeasure.FluidPerSecond, false, time: 30, fluidUnit: 22), + new DataItem("10 s", UnitOfMeasure.FluidPerSecond, false, time: 30, fluidUnit: 22), + new DataItem("10 b", UnitOfMeasure.FluidPerSecond, false, time: 30, fluidUnit: 22), + new DataItem("10 p", UnitOfMeasure.FluidPerSecond, true, 220, time: 30, fluidUnit: 22), + + new DataItem("1", UnitOfMeasure.Megawatt, true, 1), + new DataItem("10e-2k", UnitOfMeasure.Megawatt, true, .0001f), + new DataItem("-10e-2k", UnitOfMeasure.Megawatt, true, -.0001f), + new DataItem(".1e2u", UnitOfMeasure.Megawatt, true, .00000000001f), + new DataItem("10%", UnitOfMeasure.Megawatt, false), + new DataItem("10w", UnitOfMeasure.Megawatt, true, 10e-6f), + new DataItem("10 M", UnitOfMeasure.Megawatt, true, 10), + new DataItem("10/t", UnitOfMeasure.Megawatt, false), + new DataItem("10u/s", UnitOfMeasure.Megawatt, false), + new DataItem("10m/m", UnitOfMeasure.Megawatt, false), + new DataItem("10g/h", UnitOfMeasure.Megawatt, false), + new DataItem("10/ks", UnitOfMeasure.Megawatt, false), + new DataItem("10 s", UnitOfMeasure.Megawatt, false), + new DataItem("10 b", UnitOfMeasure.Megawatt, false), + new DataItem("10 p", UnitOfMeasure.Megawatt, false), + + new DataItem("1", UnitOfMeasure.Megajoule, true, 1), + new DataItem("10e-2k", UnitOfMeasure.Megajoule, true, .0001f), + new DataItem("-10e-2k", UnitOfMeasure.Megajoule, true, -.0001f), + new DataItem(".1e2u", UnitOfMeasure.Megajoule, true, .00000000001f), + new DataItem("10%", UnitOfMeasure.Megajoule, false), + new DataItem("10j", UnitOfMeasure.Megajoule, true, 10e-6f), + new DataItem("10 M", UnitOfMeasure.Megajoule, true, 10), + new DataItem("10/t", UnitOfMeasure.Megajoule, false), + new DataItem("10u/s", UnitOfMeasure.Megajoule, false), + new DataItem("10m/m", UnitOfMeasure.Megajoule, false), + new DataItem("10g/h", UnitOfMeasure.Megajoule, false), + new DataItem("10/ks", UnitOfMeasure.Megajoule, false), + new DataItem("10 s", UnitOfMeasure.Megajoule, false), + new DataItem("10 b", UnitOfMeasure.Megajoule, false), + new DataItem("10 p", UnitOfMeasure.Megajoule, false), + ]; + + private class DataItem(string input, UnitOfMeasure unit, bool expectedReturn, float expectedOutput = 0, int time = 1, int itemUnit = 0, int fluidUnit = 0, int defaultBeltSpeed = 15) { + private object[] ToArray() => [input, unit, expectedReturn, expectedOutput, time, itemUnit, fluidUnit, defaultBeltSpeed]; + public static implicit operator object[](DataItem item) => item.ToArray(); + } +} diff --git a/Yafc.Model/Data/DataUtils.cs b/Yafc.Model/Data/DataUtils.cs index fa63dce1..57b46678 100644 --- a/Yafc.Model/Data/DataUtils.cs +++ b/Yafc.Model/Data/DataUtils.cs @@ -2,13 +2,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using Google.OrTools.LinearSolver; using Yafc.UI; namespace Yafc.Model { - public static class DataUtils { + public static partial class DataUtils { public static readonly FactorioObjectComparer DefaultOrdering = new FactorioObjectComparer((x, y) => { float yFlow = y.ApproximateFlow(); float xFlow = x.ApproximateFlow(); @@ -461,57 +463,105 @@ public static string FormatAmountRaw(float amount, float unitMultiplier, string return amountBuilder.ToString(); } + [GeneratedRegex(@"^([-+0-9.e]+)([μukmgt]?)(/[hmst]|[WJsbp%]?)$", RegexOptions.IgnoreCase)] + private static partial Regex ParseAmountRegex(); + + /// + /// Tries to parse a user-supplied production rate into the standard internal format, as specified by . + /// Values are accepted in any format that YAFC can display, and consist of:
+ /// * A floating point number
+ /// * An optional SI prefix from μukMGT, case sensitive only for u and μ. (For historical reasons, m and M are both mega-; the milli- prefix is unused and unavailable.)
+ /// * An optional W, J, s, b, p, %, /s, /m, /h, or /t, case insensitive and permitted when appropriate (e.g. W only for power; no b on fluids; p only if Project.current.preferences.fluidUnit has a value). + ///
+ /// The string to parse. + /// The parsed amount, or an unspecified value if the string could not be parsed. + /// The unit that applies to this value. is not supported. + /// True if the string could be parsed as the specified unit, false otherwise. + /// Thrown when is . public static bool TryParseAmount(string str, out float amount, UnitOfMeasure unit) { + if (unit is UnitOfMeasure.Celsius) { throw new ArgumentException("Parsing to UnitOfMeasure.Celcius is not supported.", nameof(unit)); } + var (mul, _) = Project.current.ResolveUnitOfMeasure(unit); - int lastValidChar = 0; - float multiplier = unit == UnitOfMeasure.Megawatt ? 1e6f : 1f; + float multiplier = unit is UnitOfMeasure.Megawatt or UnitOfMeasure.Megajoule ? 1e6f : 1f; + + str = str.Replace(" ", ""); // Remove spaces to support parsing from the "10 000" precise format, and to simplify the regex. + var groups = ParseAmountRegex().Match(str).Groups; amount = 0; - foreach (char c in str) { - if (c is (>= '0' and <= '9') or '.' or '-' or 'e') { - ++lastValidChar; - } - else { - if (lastValidChar == 0) { - return false; - } + if (groups.Count < 4 || !float.TryParse(groups[1].Value, out amount)) { + return false; + } + + switch (groups[2].Value.SingleOrDefault()) { // μukMGT + case 'u' or 'μ': + multiplier = 1e-6f; + break; + case 'k' or 'K': + multiplier = 1e3f; + break; + case 'm' or 'M': + multiplier = 1e6f; + break; + case 'g' or 'G': + multiplier = 1e9f; + break; + case 't' or 'T': + multiplier = 1e12f; + break; + case 'U' or 'Μ' or 'K': // capital u or μ, or Kelvin symbol; false positive in the regex match + return false; + } - switch (c) { - case 'k': - case 'K': - multiplier = 1e3f; - break; - case 'm': - case 'M': - multiplier = 1e6f; - break; - case 'g': - case 'G': - multiplier = 1e9f; - break; - case 't': - case 'T': - multiplier = 1e12f; - break; - case 'μ': - case 'u': - multiplier = 1e-6f; - break; + switch (groups[3].Value.ToUpperInvariant()) { // JWsbp% or /hms + case "W" when unit is UnitOfMeasure.Megawatt: + case "J" when unit is UnitOfMeasure.Megajoule: + if (groups[2].Value.Length == 0) { + // "10", "10M" and "10MW" should all be parsed as ten megawatts, but "10W" should be parsed as ten watts. + multiplier = 1; } break; - } + case "S" when unit is UnitOfMeasure.Second: + case "%" when unit is UnitOfMeasure.Percent: + break; + case "W" or "J" or "S" or "%": + // Text units that don't match the expected units. + return false; + case not "" when unit is not UnitOfMeasure.ItemPerSecond and not UnitOfMeasure.FluidPerSecond and not UnitOfMeasure.PerSecond: + // Time-based modifiers on non-time-based units. + return false; + case "B": + if (unit != UnitOfMeasure.ItemPerSecond) { + return false; // allow belts for items only + } + if (Project.current.preferences.itemUnit > 0) { + mul = 1 / Project.current.preferences.itemUnit; + } + else { + mul = 1 / Project.current.preferences.defaultBelt.beltItemsPerSecond; + } + break; + case "P": + // allow pipes only for fluids, and only when the pipe throughput is specified + if (unit != UnitOfMeasure.FluidPerSecond || Project.current.preferences.fluidUnit == 0) { + return false; + } + mul = 1 / Project.current.preferences.fluidUnit; + break; + case "/S": + mul = 1; + break; + case "/M": + mul = 60; + break; + case "/H": + mul = 3600; + break; + case "/T": + (mul, _) = Project.current.preferences.GetPerTimeUnit(); + break; } multiplier /= mul; - string substr = str[..lastValidChar]; - if (!float.TryParse(substr, out amount)) { - return false; - } - amount *= multiplier; - if (amount > 1e15) { - return false; - } - - return true; + return amount is <= 1e15f and >= -1e15f; } public static void WriteException(this TextWriter writer, Exception ex) { diff --git a/Yafc/Data/Tips.txt b/Yafc/Data/Tips.txt index 70ace22a..b07f89ff 100644 --- a/Yafc/Data/Tips.txt +++ b/Yafc/Data/Tips.txt @@ -7,4 +7,7 @@ Tip: You can open recipe explorer using Ctrl+N or with middle mouse click on any Tip: If you close a page, it doesn't get deleted. Tip: You can undo with Ctrl+Z and redo with Ctrl+Y or Ctrl+Shift+Z Tip: Use desired products to subtract desired demand from actual production -Tip: Ctrl+Click on a tab to open it on half screen in addition to currently opened tab \ No newline at end of file +Tip: Ctrl+Click on a tab to open it on half screen in addition to currently opened tab +Tip: Specify /m to produce that many items (or fluid units) per minute, regardless of the current display mode +Tip: Specify b to produce that many belts worth of product, for the belt you've selected in the preferences +Tip: Specify k to multiply the production by 1000. Other SI prefixes work too! (put the prefix before 'b' or '/h') diff --git a/changelog.txt b/changelog.txt index f6e55649..2b9cfaba 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,9 @@ ---------------------------------------------------------------------------------------------------------------------- Version: 0.6.5 Date: soon + Features: + - Add the option to specify a number of belts of production, and to specify per-second/minute/hour + production regardless of the current display setting. Changes: - Add a help message and proper handling for command line arguments - Removed default pollution cost from calculation. Added a setting to customize pollution cost. From a3b991d3fdc305b22636a393f890fec50aa8500b Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 13 May 2024 18:34:11 -0400 Subject: [PATCH 2/2] Add a note to the tips that /h and /s work like /m. --- Yafc/Data/Tips.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Yafc/Data/Tips.txt b/Yafc/Data/Tips.txt index b07f89ff..5f862ce6 100644 --- a/Yafc/Data/Tips.txt +++ b/Yafc/Data/Tips.txt @@ -8,6 +8,6 @@ Tip: If you close a page, it doesn't get deleted. Tip: You can undo with Ctrl+Z and redo with Ctrl+Y or Ctrl+Shift+Z Tip: Use desired products to subtract desired demand from actual production Tip: Ctrl+Click on a tab to open it on half screen in addition to currently opened tab -Tip: Specify /m to produce that many items (or fluid units) per minute, regardless of the current display mode +Tip: Specify /m to produce that many items (or fluid units) per minute, regardless of the current display mode (You can also use '/s' for per second and '/h' for per hour) Tip: Specify b to produce that many belts worth of product, for the belt you've selected in the preferences Tip: Specify k to multiply the production by 1000. Other SI prefixes work too! (put the prefix before 'b' or '/h')