Duke's Big Numbers 1.0
C++ and Blueprint libraries for performing math, analysis, and formatting with really large numbers (>10e308).
|
As an avid fan of incremental games, I understand the importance of solid formatting of the big numbers that come with such a game. So I focused heavily on making sure that DBN can format numbers well, cover as many use cases as possible, and provide points of extensibility for the use cases I cannot foresee.
Let's face it: there is no true standard when it comes to representing numbers this large, and there is value in having a stylized formatting of such large numbers, as these numbers are very much a main character in the games that they are featured in.
So, DBN adopts a pluggable formatting system where the individual bits of formatting can be tailored and tuned to the individual application's need.
This system is specifically expected to be more extensable, performant, and supple than a system of format specifier strings.
DBN comes with several pack-in formatters that will address many needs with the configuration options provided.
The base class for all formatters is the UOBJECT UBigIntegerFormatter. Derived from this class are several useful stock formatters, or you can derive your own.
The simplest use case is to use the static methods exposed from each formatter type, or the corresponding Blueprint Library methods, to directly format numbers to strings. These methods will retrieve and use the default formatter for the desired type. These default formatters (which are distinct objects from UE CDOs) can have their settings modified, and the modifications will apply to all uses of that default formatter.
C++ code and Blueprints can also create a formatter or retrieve a default formatter, use that formatter to directly format BigIntegers to strings, and store the formatter for future use.
The formatters expose methods both to return the formatted number as a new String, or to append it to an existing string.
The UDecimalInviantFormatter and UHexadecimalInvariant formatters provide no-frills formatting of Decimal and Hexadecimal radix strings, respectively. By design, there are no options to configure that would slow down formatting.
These formatters are intended for serialization of BigInteger values, and the output from these is guaranteed to be parsable by the Duke::BigNumbers::FBigIntegerParser::ParseInvariantDecimal() and Duke::BigNumbers::FBigIntegerParser::ParseInvariantHexadecimal() methods and their kin.
Both formatters output the least number of digits required, prepended with a sign if negative.
The default formatters for these classes can be retrieved through the UDecimalInvariantFormatter::GetDefault() and UHexadecimalInvariantFormatter::GetDefault() methods, respectively.
For convenience, the functionality of these formatters is exposed through the FBigInteger::ToString() and FBigInteger::ToHexString() instance methods.
UDecimalFullFormatter, as its name implies, formats the full precision of a decimal number. It does this in culture-aware format, according to the options configured on the formatter. Use this formatter to create a full-precision rendering of a number for human consumption.
The default formatters for this class can be retrieved through the UDecimalFullFormatter::GetDefault() method.
UDecimalFullFormatter::SetUseGrouping() can be used to enable/disable the emission of digit group separators. If enabled, group separators are added per current culture settings.
UDecimalFullFormatter::SetAlwaysSign() can be used to enable/disable whether numbers are always displayed as signed. If enabled, this overrides culture settings if the culture settings specify that no sign string is to be emitted for positive numbers. In this case, the sign string used for negative numbers will have the culture-specific negative symbol replaced with the culture-specific positive symbol to form the sign string used for positive numbers. This is the same behavior as UE's internal formatting methods.
UDecimalScalingFormatter will probably be of most interest incremental game designers. It allows numbers to be abbreviated in many stylized ways based on powers of ten. This formatter is intended to handle all decimal scaling display needs, whether it be scientific notiation, engineering notation, named notation, SI units (names or symbols) notation, or custom grouping notation.
Has a pluggable GroupLabeler component for getting group names for powers of ten, and allows a fallback formatter to be specified for values outside of the range intended to be handled by the formatter.
Several default instances of UDecimalScalingFormatter are provided as convenient starting points:
These instances differ mainly in which UGroupLabeler they use.
Currently, Decimal Scaling Formatter rounds to zero. I reason that this is the behavior wanted in a vast majority of games using numbers of this magnitude, because one does not want to present to the user that they surpassed a threshold until they actually have (i.e. one would usually want 999,999,999 to display as 999.999 million, not 1.000 billion, even though 999,999,999 rounded to the nearest million is 1 billion.) Other rounding modes may be implemented in a future version.
Decimal Scaling Formatter offers several settings:
UDecimalScalingFormatter::SetUseGrouping() can be used to enable/disable the emission of digit group separators in the mantissa. If enabled, group separators are added per current culture settings. This will usually only have effect if a ScaleGranularity greater than three is used.
UDecimalScalingFormatter::SetAlwaysSign() can be used to enable/disable whether the mantissa is always displayed as signed. If enabled, this overrides culture settings if the culture settings specify that no sign string is to be emitted for positive numbers. In this case, the sign string used for negative numbers will have the culture-specific negative symbol replaced with the culture-specific positive symbol to form the sign string used for positive numbers. This is the same behavior as UE's internal formatting methods.
UDecimalScalingFormatter::SetMinPowerForScaleMode() is used to minmum power of ten required for this formatter to be used to format the number. If the number is below this power of ten, the formatting request will be forwarded to the FallbackFormatter.
UDecimalScalingFormatter::SetMaxPowerForScaleMode() is used to maximum power of ten required for this formatter to be used to format the number. If the number is above this power of ten, the formatting request will be forwarded to the FallbackFormatter.
UDecimalScalingFormatter::SetScaleGranularity() is used to specify how many powers of ten are required to advance to a different grouping. Defaults to three. Most of the default formatters use three, except notably Scientific, which uses a value of one.
UDecimalScalingFormatter::SetGroupLabeler() is used to specify how groupings of powers of ten should be labeled. See GroupLabeler section.
UDecimalScalingFormatter::SetFallbackFormatter() is used to specify a formatter to be used if a number falls outside of the digit range this formatter is willing to handle.
UDecimalScalingFormatter::SetMinimumFractionalDigits() is used to set the minimum fractional digits used to display the mantissa. Defaults to three.
UDecimalScalingFormatter::SetMinimumFractionalDigits() is used to set the maximum fractional digits used to display the mantissa. Defaults to three.
UBigIntegerFormatter provides the abstract base (derived from UObject) for any BigInteger formatter object. By implementing this base, any new formatter can get plugged in to the same formatting framework as and interchanged with the pack-in formatters.
Due to the performance and low-level nature generally required of number formatting, it is recommended that custom formatters be implemented in C++.
There is only one method to implement in order to create a derived UBigIntegerFormatter. Overriding UBigIntegerFormatter::OnAppendToString() will allow you to receive and respond to formatting requests.
The UGroupLabeler abstract class interface is the power behind the UDecimalScalingFormatter's ability to flexibly interchange between number notations. A Group Labeler that derives from UGroupLabeler translates powers of ten into readable labels.
All group labelers respect the SeparatorBefore property. UGroupLabeler::SetSeparatorBefore() allows setting a prefix string to be appended to a string before the actual group label. This value defaults to a single space.
There are several pack-in group labelers provided with DBN.
At the heart of the UDecimalScalingFormatter lies another abstraction - the UGroupLabeler. This UOBJECT takes as input a power of ten, and returns a label for that power of ten.
Several useful stock UGroupLabeler implementations are provided, or you can derive your own. For simple customization cases, UCustomGroupLabeler may suffice for your needs.
Group labelers will usually be used in the context of a UBigIntegerFormatter, but can also be used independently.
UScientificGroupLabeler powers formatting both scientific and engineering notation numbers. One of the simplest group labelers, it formats the power of ten using "e999" notation.
UDecimalScalingFormatter::SetUseGrouping() can be used to enable/disable the emission of digit group separators in the exponent. If enabled, group separators are added per current culture settings.
UDecimalScalingFormatter::SetAlwaysSign() can be used to enable/disable whether the exponent is always displayed as signed. If enabled, this overrides culture settings if the culture settings specify that no sign string is to be emitted for positive numbers. In this case, the sign string used for negative numbers will have the culture-specific negative symbol replaced with the culture-specific positive symbol to form the sign string used for positive numbers. This is the same behavior as UE's internal formatting methods.
Works for the full Integer input range, making it an ideal last-resort fallback.
UNamedGroupLabeler will take a power of ten, and convert it to a name in the extended "standard" American numbering system. A value of three will return "thousand", a power of six will return "million", etc. Negative powers of ten will also append "th", i.e. a value of negative 3 will return "thousandth".
Currently works for powers of ten up to 30002.
USIUnitsGroupLabeler will take a power of ten, and convert it to an SI Unit name or symbol, depending on the setting of Mode.
Works in the standard SI units range as of 2022, from -30 (quecto) to 30 (quetta).
UCustomGroupLabeler provides a simple solution for custom mapping of powers of ten to application-defined values as an alternative to deriving from UGroupLabeler. It uses a supplied array to form a new group name for every three powers of ten. Works only for positive values up to the number of array elements provided times three.
The GroupLabels property controls the array of labels that are used.
If you derive from this class, you can override the UCustomGroupLabeler::MapPowerToLabelIndex() method to change the default behavior of mapping every three powers of ten to a new label.
If you derive from UGroupLabeler directly, you need only override the UGroupLabeler::OnAppendGroupLabel method. In this overriden method, you will take the input power of ten, translate it to a group name, append that group name to a string, and then return a remainder (more on that in a moment).
Due to the performance and low-level nature generally required of number formatting, it is recommended that custom group labelers be implemented in C++.
The methods that Append a group label return an Integer remainder. What this value represents is the difference between the power of ten passed into the group labeler, and the actual power of ten that the appended label represents. So, for example, if you pass a value of 5 to a USIUnitsGroupLabeler in Name mode, the label "kilo" would be appended, and since "kilo" represents 10^3, the remainder returned would be 2. Using the same USIUnitsGroupLabeler, if you pass a value of 46, the label "quetta" would be appended, and since "quetta" represents 10^30, the remainder returned would be 16.