diff --git a/Content.Shared/Localizations/Localization.cs b/Content.Shared/Localizations/Localization.cs index c089f1d532..cbcb8e0cbb 100644 --- a/Content.Shared/Localizations/Localization.cs +++ b/Content.Shared/Localizations/Localization.cs @@ -33,6 +33,7 @@ namespace Content.Shared.Localizations loc.AddFunction(culture, "PRESSURE", FormatPressure); loc.AddFunction(culture, "POWERWATTS", FormatPowerWatts); loc.AddFunction(culture, "POWERJOULES", FormatPowerJoules); + loc.AddFunction(culture, "UNITS", FormatUnits); loc.AddFunction(culture, "TOSTRING", args => FormatToString(culture, args)); loc.AddFunction(culture, "LOC", FormatLoc); } @@ -85,5 +86,45 @@ namespace Content.Shared.Localizations { return FormatUnitsGeneric(args, "zzzz-fmt-power-joules"); } + + private static ILocValue FormatUnits(LocArgs args) + { + if (!Units.Types.TryGetValue(((LocValueString) args.Args[0]).Value, out var ut)) + throw new ArgumentException($"Unknown unit type {((LocValueString) args.Args[0]).Value}"); + + var fmtstr = ((LocValueString) args.Args[1]).Value; + + double max = Double.NegativeInfinity; + var iargs = new double[args.Args.Count - 1]; + for (var i = 2; i < args.Args.Count; i++) + { + var n = ((LocValueNumber) args.Args[i]).Value; + if (n > max) + max = n; + + iargs[i - 2] = n; + } + + if (!ut!.TryGetUnit(max, out var mu)) + throw new ArgumentException("Unit out of range for type"); + + var fargs = new object[iargs.Length]; + + for (var i = 0; i < iargs.Length; i++) + fargs[i] = iargs[i] * mu.Factor; + + fargs[^1] = Loc.GetString($"units-{mu.Unit.ToLower()}"); + + // Before anyone complains about "{"+"${...}", at least it's better than MS's approach... + // https://docs.microsoft.com/en-us/dotnet/standard/base-types/composite-formatting#escaping-braces + // + // Note that the closing brace isn't replaced so that format specifiers can be applied. + var res = String.Format( + fmtstr.Replace("{UNIT", "{" + $"{fargs.Length - 1}"), + fargs + ); + + return new LocValueString(res); + } } } diff --git a/Content.Shared/Localizations/Units.cs b/Content.Shared/Localizations/Units.cs new file mode 100644 index 0000000000..c998e4ecfe --- /dev/null +++ b/Content.Shared/Localizations/Units.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.Localizations +{ + public static class Units + { + public sealed class TypeTable + { + public readonly Entry[] E; + + public TypeTable(params Entry[] e) => E = e; + + public sealed class Entry + { + // Any item within [Min, Max) is considered to be in-range + // of this Entry. + public readonly (double? Min, double? Max) Range; + + // Factor is a number that the value will be multiplied by + // to adjust it in to the proper range. + public readonly double Factor; + + // Unit is an ID for Fluent. All Units are prefixed with + // "unit-" internally. Usually follows the format $"{unit-abbrev}-{prefix}". + // + // Example: "si-g" is actually processed as "units-si-g" + // + // As a matter of style, units for values less than 1 (i.e. mW) + // should have two dashes before their prefix. + public readonly string Unit; + + public Entry((double?, double?) range, double factor, string unit) + { + Range = range; + Factor = factor; + Unit = unit; + } + } + + public bool TryGetUnit(double val, [NotNullWhen(true)] out Entry? winner) + { + Entry? w = default!; + foreach (var e in E) + if ((e.Range.Min == null || e.Range.Min <= val) && (e.Range.Max == null || val < e.Range.Max)) + w = e; + + winner = w; + return w != null; + } + + public string Format(double val) + { + if (TryGetUnit(val, out var w)) + return (val * w.Factor).ToString() + " " + w.Unit; + + return val.ToString(); + } + + public string Format(double val, string fmt) + { + if (TryGetUnit(val, out var w)) + return (val * w.Factor).ToString(fmt) + " " + w.Unit; + + return val.ToString(fmt); + } + } + + public static readonly TypeTable Generic = new TypeTable + ( + // Table layout. Fite me. + new TypeTable.Entry(range: ( null, 1e-24), factor: 1e24, unit: "si--y"), + new TypeTable.Entry(range: (1e-24, 1e-21), factor: 1e21, unit: "si--z"), + new TypeTable.Entry(range: (1e-21, 1e-18), factor: 1e18, unit: "si--a"), + new TypeTable.Entry(range: (1e-18, 1e-15), factor: 1e15, unit: "si--f"), + new TypeTable.Entry(range: (1e-15, 1e-12), factor: 1e12, unit: "si--p"), + new TypeTable.Entry(range: (1e-12, 1e-9), factor: 1e9, unit: "si--n"), + new TypeTable.Entry(range: ( 1e-9, 1e-3), factor: 1e6, unit: "si--u"), + new TypeTable.Entry(range: ( 1e-3, 1), factor: 1e3, unit: "si--m"), + new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "si"), + new TypeTable.Entry(range: ( 1000, 1e6), factor: 1e-4, unit: "si-k"), + new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "si-m"), + new TypeTable.Entry(range: ( 1e9, 1e12), factor: 1e-9, unit: "si-g"), + new TypeTable.Entry(range: ( 1e12, 1e15), factor: 1e-12, unit: "si-t"), + new TypeTable.Entry(range: ( 1e15, 1e18), factor: 1e-15, unit: "si-p"), + new TypeTable.Entry(range: ( 1e18, 1e21), factor: 1e-18, unit: "si-e"), + new TypeTable.Entry(range: ( 1e21, 1e24), factor: 1e-21, unit: "si-z"), + new TypeTable.Entry(range: ( 1e24, null), factor: 1e-24, unit: "si-y") + ); + + // N.B. We use kPa internally, so this is shifted one order of magnitude down. + public static readonly TypeTable Pressure = new TypeTable + ( + new TypeTable.Entry(range: (null, 1e-6), factor: 1e9, unit: "u--pascal"), + new TypeTable.Entry(range: (1e-6, 1e-3), factor: 1e6, unit: "m--pascal"), + new TypeTable.Entry(range: (1e-3, 1), factor: 1e3, unit: "pascal"), + new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "k-pascal"), + new TypeTable.Entry(range: (1000, 1e6), factor: 1e-4, unit: "M-pascal"), + new TypeTable.Entry(range: ( 1e6, null), factor: 1e-6, unit: "G-pascal") + ); + + public static readonly TypeTable Power = new TypeTable + ( + new TypeTable.Entry(range: (null, 1e-3), factor: 1e6, unit: "u--watt"), + new TypeTable.Entry(range: (1e-3, 1), factor: 1e3, unit: "m--watt"), + new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "watt"), + new TypeTable.Entry(range: (1000, 1e6), factor: 1e-4, unit: "k-watt"), + new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "m-watt"), + new TypeTable.Entry(range: ( 1e9, null), factor: 1e-9, unit: "g-watt") + ); + + public static readonly TypeTable Energy = new TypeTable + ( + new TypeTable.Entry(range: (null, 1e-3), factor: 1e6, unit: "u--joule"), + new TypeTable.Entry(range: (1e-3, 1), factor: 1e3, unit: "m--joule"), + new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "joule"), + new TypeTable.Entry(range: (1000, 1e6), factor: 1e-4, unit: "k-joule"), + new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "m-joule"), + new TypeTable.Entry(range: ( 1e9, null), factor: 1e-9, unit: "g-joule") + ); + + public readonly static Dictionary Types = new Dictionary + { + ["generic"] = Generic!, + ["pressure"] = Pressure!, + ["power"] = Power!, + ["energy"] = Energy! + }; + } +} diff --git a/Resources/Locale/en-US/_units.ftl b/Resources/Locale/en-US/_units.ftl new file mode 100644 index 0000000000..1e687a05d4 --- /dev/null +++ b/Resources/Locale/en-US/_units.ftl @@ -0,0 +1,80 @@ +## Standard SI prefixes +units-si--y = y +units-si--z = z +units-si--a = a +units-si--f = f +units-si--p = p +units-si--n = n +units-si--u = µ +units-si--m = m +units-si = {""} +units-si-k = k +units-si-m = M +units-si-g = G +units-si-t = T +units-si-p = P +units-si-e = E +units-si-z = Z +units-si-y = Y + +### Long form +units-si--y-long = yocto +units-si--z-long = zepto +units-si--a-long = atto +units-si--f-long = femto +units-si--p-long = pico +units-si--n-long = nnano +units-si--u-long = micro +units-si--m-long = milli +units-si-long = {""} +units-si-k-long = kilo +units-si-m-long = mega +units-si-g-long = giga +units-si-t-long = tera +units-si-p-long = peta +units-si-e-long = exa +units-si-z-long = zetta +units-si-y-long = yotta + +## Pascals (Pressure) +units-u--pascal = µPa +units-m--pascal = mPa +units-pascal = Pa +units-k-pascal = kPa +units-m-pascal = MPa +units-g-pascal = GPa + +units-u--pascal-long = Micropascal +units-m--pascal-long = Millipascal +units-pascal-long = Pascal +units-k-pascal-long = Kilopascal +units-m-pascal-long = Megapascal +units-g-pascal-long = Gigapascal + +## Watts (Power) +units-u--watt = µW +units-m--watt = mW +units-watt = W +units-k-watt = kW +units-m-watt = MW +units-g-watt = GW + +units-u--watt-long = Microwatt +units-m--watt-long = Milliwatt +units-watt-long = Watt +units-k-watt-long = Kilowatt +units-m-watt-long = Megawatt +units-g-watt-long = Gigawatt + +## Joule (Energy) +units-u--joule = µJ +units-m--joule = mJ +units-joule = J +units-k-joule = kJ +units-m-joule = MJ + +units-u--joule-long = Microjoule +units-m--joule-long = Millijoule +units-joule-long = Joule +units-k-joule-long = Kilojoule +units-m-joule-long = Megajoule