Utilities for handling math and showing numbers in Java.
... And a bunch of other things, mostly aimed at cross-platform compatibility.
If you are making a new project with gdx-liftoff, just check the digital
checkbox under Third-Party Extensions.
To add to an existing project...
In your core/build.gradle
's dependencies, add:
api "com.github.tommyettinger:digital:0.9.1"
If you have a GWT module, then in html/build.gradle
's dependencies, add:
api "com.github.tommyettinger:digital:0.9.1:sources"
And also for GWT, add this line to GdxDefinition.gwt.xml
:
<inherits name="com.github.tommyettinger.digital" />
Then, you can import classes from this library in your core module or any other submodules that depend on core. You might be depending on this for any number of reasons, but platform-independent code is a common thread.
There are only a few classes here, and any given application only is likely to use even fewer of them. Still, they're useful as a package, since they often depend on each other.
BitConversion allows converting float and double values to
int and long versions of their underlying bits (and it does
this in a way that works on GWT efficiently). While code that
does this is available in the regular JDK in Float and Double,
the implementations for those provided by GWT are extremely
slow. BitConversion was initially meant just to provide a
faster route for those conversions on GWT, but it also has
some methods that convert float-int and double-long with
reversed byte order (using a fast intrinsic on desktop JDKs
and a special trick on GWT), and others that get only the low
or high half of a double's bits as an int. It is modeled after
the NumberUtils class from libGDX, but offers extra methods.
It also has, purely for GWT purposes, an imul()
method that
acts just like integer multiplication on most platforms, but
compiles down to a call to the JavaScript Math.imul()
function on GWT, which automatically handles overflow in the
same way a desktop JVM would, and won't lose precision.
Similarly, countLeadingZeros()
compiles to the JavaScript
Math.clz32()
function on GWT (or for long
arguments, some
small code that calls that), while using
Integer.numberOfLeadingZeros()
(or its Long
counterpart)
on non-GWT platforms. Counting leading zeros is
an operation that shows up in a surprising assortment of
places and is supposed to be fast, so having an alternative to
this monstrosity
is more than welcome. JDK 19-24 (at least for some versions
of those JDK releases) has a known bug
in Integer.numberOfLeadingZeros()
that is avoided currently
and a fixed version used, while still taking advantage of
intrinsics on HotSpot and Math.clz32()
on GWT. There's also
countTrailingZeros()
for int and long arguments, which
compiles into a single call to
Integer.numberOfTrailingZeros()
(or using Long
) on most
platforms, or a JS two-liner on GWT that uses Math.clz32()
.
Another method purely for GWT support, lowestOneBit()
works
around a bug that was present in GWT 2.8.2 and may still be
present, where its built-in Long.lowestOneBit()
method could
return very wrong results for larger inputs.
Base is much larger, and allows converting any Java primitive
number type to a specific base/radix/number-system. Here,
Bases are flexible enough to be able to be generated randomly
for obfuscation purposes (such as to mangle a high score in a
save file). There are several Base constants already defined.
Each one can write numbers as signed (variable-length) or
unsigned (fixed-length), as well as read back either type from
one method. Parsing is significantly more relaxed than in the
JDK, and invalid numbers tend to be returned as 0 rather than
requiring an Exception to be caught. Base has some options
when writing (or reading) a float
or double
. Like other
numeric types, you can use signed()
or unsigned()
to write
using the digits the Base usually uses, though here it writes
the bits that compose the float or double for higher accuracy.
Unlike other numeric types, there are base-10 decimal()
,
scientific()
, general()
, and friendly()
methods to write
floats/doubles with different rules for when to switch to
scientific notation, if at all. These can be read back with
readFloat()
and readDouble()
, while the signed/unsigned
output needs readFloatExact()
or readDoubleExact()
. If you
use a scrambled base (a random one, as mentioned before), then
you need to use signed()
/unsigned()
/readFloatExact()
/
readDoubleExact()
to use the right scrambled digits.
decimal()
can take an exact length to limit large output to,
or zero-pad small output to, as well as a precision for how
many digits to show after the decimal point. Base
can also combine the String representations of an array of
primitives (or part of such an array, since 0.3.1) using
join()
or appendJoined()
, and split apart Strings into
arrays of primitives with methods like longSplit()
,
intSplit()
, and floatSplit()
. There are also now static
methods with readable
in their names; these output text
that can be used in Java source code as numeric or character
literals, for code generation. The Base.BASE10.readLong()
method can read a long produced by Base.readable(long)
just
fine, because readLong() ignores invalid characters after the
number (such as L
in longs). Reading in the char literals
this can produce requires using Base.readCharReadable()
.
Base can append to existing StringBuilder, StringBuffer,
CharBuffer, or other types, as long as they implement both
CharSequence and Appendable.
TrigTools tries to be as complete as possible at covering
trigonometric functions, offering sin, cos, tan, asin, acos,
atan, and atan2 in radians, degrees, and turns. It also allows
access to the lookup tables used by sin and cos. Much of
TrigTools can be seen as similar to what libGDX's MathUtils
class offers, but allowing access to the lookup tables permits
a few novel features (see its docs). It supports float and
double arguments/returns for all functions. It also provides
"smooth" sin and cos approximations that aren't table-based,
"smoother" sin/cos/tan versions that interpolate between
entries in the lookup table for very high precision, and
"precise" versions of all methods that avoid lookup tables and
aim for near-maximum accuracy at the cost of some speed. Note
that very few libraries support trigonometric functions that
take angles in turns, but turns can be very useful for a
variety of cases. For example, if you want to store a hue
(which is essentially an angle) into a limited format such as
one channel of a color to be sent to the GPU, storing the hue
as an angle in turns keeps it in the 0.0 to 1.0 range, but
using radians or degrees would not. There's also a few extra
methods, such as atan2Deg360()
, which acts like the degree
version of atan2, but always returns an angle between 0
(inclusive) and 360 (exclusive), since negative angles are
less intuitive and sometimes not supported. For users
concerned about non-deterministic behavior of floating-point
"special functions," TrigTools avoids calling Math.sin()
and
Math.cos()
entirely, and any lookup tables it builds use the
"precise" approximations to avoid the possibility of different
results on different hardware, JVM version, or the like. This
guarantee can't apply to transpiled code like GWT produces,
because at least in the case of GWT, there isn't an available
32-bit float
type, and methods that should return a float
instead return some form of approximation to a float
that
JavaScript permits.
MathTools offers a wild grab bag of math functions and constants, from simple lerp, floor, ceil, and clamp methods to an optimized cube root function and parameterized splines. It is also based on MathUtils from libGDX. It supports float and double arguments/returns for most functions; some make sense for float only, like the optimized cube root. There's also a lot of commonly-defined constants, such as the square root of 2 and the golden ratio, as floats and doubles. Some methods here are useful in other mathematical code, like gamma and greatestCommonDivisor; others are more esoteric, like modularMultiplicativeInverse. There are some of the standard signal processing functions, such as triangle waves, square waves, and sawtooth waves. There are also a few functions here, "sway" and relatives, that look like sine waves when graphed, but are simpler to calculate internally. You can do some bitwise operations with MathTools, such as interleaving the bits of two numbers (also called a Morton code, or a position on the Z-Order Curve). There are ways to bring a float or double closer to 0.0 by the smallest possible amount. There's the fract() methods, familiar to shader programmers, that get the fractional part of a float or double. There's Freya Holmer's recently-discovered method to make a float approach another value at an even rate using interpolation. Various methods are there relating to sigmoid functions, including two configurable methods (basicSigmoid() and rootSigmoid()), and an alternative to the just-mentioned approach() that uses a sigmoid function to gradually approach a target value. In general, if you need a math function that isn't trigonometry-related and doesn't do something with text, you may want to look here first.
ArrayTools provides common code for dealing with 2D arrays, and also sometimes 1D or 3D arrays. It allows copying, inserting, and filling 2D arrays, and creating ranges of 1D arrays. It also has a lot of methods for shuffling 1D arrays, 2D arrays, and sections of 1D arrays, for all primitive types and for objects. There are also some "filler supplies" in ArrayTools -- methods for filling up char, int, or String arrays with contents that are guaranteed to be distinct up to a certain limit. The same data that can be used to fill up arrays with ArrayTools also gets used by Hasher for seeding its predefined instances. The filler supplies include the names of Greek letters, the chemical elements of the periodic table, and the listing of names of demons from the Ars Goetia (many of which are familiar names from games that also used that list).
Hasher is... large. It provides fast, high-quality hashing functions for primitive arrays (and arrays of objects, if they implement hashCode()), and has 64-bit and 32-bit variants. The specific hashing algorithm it uses is a somewhat-hardened version of wyhash that doesn't use 128-bit math.
While Hasher can hash all types of 1D
primitive array and most types of 2D primitive array using dedicated
methods, 3D (and higher) dimensions are hashed using a generalized
method that takes another method, usually one of the previously
mentioned hashes for primitive arrays. The 3D code is already defined
for primitives, using calls like
Hasher.byteArray3DHashBulk64.hash64(longSeed, bytes3D)
, where
longSeed is a long to seed the hash, and bytes3D is a byte[][][]
.
That byteArray3DHashBulk64() is a lambda of the type
SeededHashFunction64<byte[][][]>
that calls
hashBulk64(seed, byteArray2DHashBulk64, data)
; you could change 2D
to 3D there to make your own hash for 4D byte arrays (treating
them as arrays of 3D byte arrays). Hasher
allows you to specify the seed when you call hash()
or
hash64()
(as static methods), but it also has a fairly-large
array of predefined
hash functions with instance methods for
hash()
and hash64()
. Starting in 0.3.0, Hasher can either
process a whole input array or part of one, specified by a start
index and a length. Starting in 0.5.1, Hasher also provides
hashBulk()
and hashBulk64()
methods that use a different,
slightly-higher-quality algorithm, and these methods can be
faster than hash
and hash64
on large inputs. How large
depends on your use case, but the quality improvement may be
enough reason to switch on its own.
Hasher also has a few unary hashes that
can be used as quick and dirty random number generators when
applied to numbers in a sequence. The unary hashes can output
longs, bounded ints, floats, and doubles, so they may be useful in
a lot of cases. They are named like randomize1()
, randomize2()
,
randomize3()
, and so on, with higher numbers being typically
slightly slower but also higher-quality (and more permissive of
sets of inputs with atypical patterns). Most usage is probably
served best by randomize3()
and its bounded int, float, and
double variants, since it uses a proven algorithm (MX3) and isn't
much different on runtime performance.
While Hasher should usually
change only very rarely, if at all, I was concerned about a few
properties of the hashing code that might have meant some values
would be returned less frequently, though this wasn't detectable to
the SMHasher test suite. In
version 0.4.0, Hasher's output for any given seed will be different
from the same seed in previous versions. You can get the older
Hasher instances either from Git history or the test folder's
OldHasher v020
or OldHasher v037 files,
if you need to reproduce the results of older seeds. Newer versions
of SMHasher do find statistical failures in at least some hash()
and
hash64()
methods in Hasher
, but the recently-added hashBulk()
and hashBulk64()
methods don't have detectable issues in SMHasher 3.
The bulk methods changed in version 0.6.0, so they also pass the older
SMHasher 2. The changes mainly affect a test for "bad seeds," which
the bulk hashes pass without trouble now. This does mean 0.5.x and
0.6.0 produce different output for bulk hashes.
AlternateRandom is a quick micro-port of a random number generator
from the closely-related juniper
library. It is used only in ArrayTools here, as the default when
no Random
object is specified. The alternative would be to use
a java.util.Random
object, but that can't produce as many
possible shuffles of mid-size arrays, and is slower, both of which
AlternateRandom solves to some extent. If you don't use juniper,
then AlternateRandom is a pretty good replacement for Random
;
if you do use juniper, then its AceRandom
generator is similar to or the same as AlternateRandom's
algorithm, and offers many more features.
ShapeTools provides some predefined mathematical constants for the vertices and faces of 3D polyhedra (currently, just the 5 Platonic solids). It could be useful for code that needs 3D shapes in code for something like continuous noise, but doesn't have access to 3D models.
TextTools provides some features that are similar to ones in Base, but different enough to belong in their own class. It operates on CharSequences usually, but sometimes needs Strings, so I'll just call this vague type "text." This class includes many different ways to search text for something, code to count the occurrences of text in a larger piece of text, code to join/split arrays of text and larger texts (this also works for boolean arrays), code for padding text, code for replacing text with literals, and code to repeat pieces of text multiple times (possibly with a delimiter). This class mostly exists to avoid duplicating similar code that occurs often throughout my projects, and is related to code here.
Interpolations, along with its nested classes InterpolationFunction
and Interpolator, provide a way to store and look up functions to
smoothly interpolate between floats. This code is very similar to
the Interpolation class in libGDX, and all the instances in
Interpolation have a counterpart in Interpolations (there are some
more here, as well). Creating an Interpolator registers its name
in Interpolations' registry, where it can be looked up on its own
with get(String)
or as a group with getInterpolatorArray()
.
There are also ways to create one of the building blocks of an
Interpolator, an InterpolationFunction, with methods in
Interpolations. This makes creating new Interpolators with different
parameters as easy as assigning a name to the generated function.
Here's a sample page that shows how each Interpolator looks graphed.
Using that page of graphs is a good idea when you're deciding which
of the many Interpolator varieties to use.
RoughMath has a variety of "rough" and "rougher" approximations to a
variety of functions. These tend to be faster but less precise.
The logistic functions logisticRough()
and logisticRougher()
are
actually not that imprecise for small-ish inputs, and are a handy
tool that is only available in RoughMath. There's also hyperbolic
trigonometric functions, which aren't anywhere else either, plus
expRough()
and a variant pow2Rough()
. MathTools also has
approximations to Math.exp()
, including one that has a limited
domain (it returns 0 outside that domain, instead of returning
gradually smaller numbers; this can sometimes be useful).
Stringf is a somewhat experimental class that exists mostly to be a
partial polyfill for the missing String.format()
method on GWT and
on some versions of TeaVM. It doesn't have anywhere near the full
functionality of String.format()
, or even the functionality of
Formic's Stringf.format()
,
but unlike Formic it should work on TeaVM. It does have enough support
for the simpler usages of %d
and %f
, as well as common conveniences
such as %08X
to print a 32-bit int as 8 hex digits, and always 8.
You could also use Base.BASE16.unsigned(num)
for that.
Distributor is a relatively narrow class that exists to redistribute
int, long, and double inputs to float or double normal-distributed
values, while preserving patterns in the input data. The probit
family of
methods can take int or long input, and act by mapping the lowest possible
inputs to the lowest normal-distributed values it can produce, and similarly
for the highest; inputs near 0 produce results near 0. There are probably all
sorts of creative uses for mapping int or long inputs to normal-distributed
outputs; you can find some yourself! Distributor also provides the Ziggurat
algorithm, which has higher-quality output in the trail of the distribution,
but does not preserve patterns from input to output. The normal()
method
using Ziggurat is about as fast as it gets for generating Gaussian variates.
Base, AlternateRandom, and Hasher can all have their instances written to a
String in a compact serialized form, or appended to a StringBuilder,
StringBuffer, CharBuffer, or other Appendable CharSequence. They can also
have that compact form read back, either into an existing AlternateRandom or
Base, or into a new Hasher (because Hasher is immutable).
Interpolations.Interpolator can't be serialized like this universally, but
if you use only predefined Interpolator instances or register all
Interpolator types you use before loading any, you can write the
Interpolator.getTag()
and load it using Interpolations.get(String)
.
To depend on digital with Gradle, add this to your dependencies (in
your core module's build.gradle
, for libGDX projects):
api "com.github.tommyettinger:digital:0.9.1"
If you target GWT using libGDX, you will also need this in your
html module's build.gradle
:
api "com.github.tommyettinger:digital:0.9.1:sources"
If you target GWT, it needs to be told about these changes in your GdxDefinition.gwt.xml
file. For any remotely-recent version of digital (0.1.7 and later), use:
<inherits name="com.github.tommyettinger.digital" />
You can also use JitPack to get a recent commit; in that case, follow its instructions here. This also has instructions for Maven and other build tools.
This library needs Java language level 8, but does not rely on any APIs introduced in Java 8. Targeting level 8 means this will work even if your project uses the newest Java versions (20 and later do not support targeting Java 7). Android projects should be able to use digital even without needing core library desugaring, as long as they target release 8 or higher. GWT can use digital without issue from GWT 2.8.0 up; GWT compatibility is a major focus of this library. RoboVM, for iOS, can use digital because no APIs are used from Java 8.
In a libGDX project, you must make sure the sourceCompatibility is 8 or higher in your core module and any other modules that use digital. This is currently not the default for gdx-setup projects (which is no longer the official setup tool), but is the default for gdx-liftoff projects (which is the official setup tool). Liftoff also lets you just check a box to depend on digital.
Apache 2.0. This includes some modified code from
libGDX,
SquidLib,
SquidSquad,
Uncommon Maths,
wyhash,
Apache Commons Lang,
fastapprox,
root-cellar,
Jolt Physics,
and Ryu. More code
is not from a particular repository (for example, some is from
Wikipedia); see each file for specific author credits. The Ryu
code is more substantial than the other projects, and even
though its license is also Apache 2.0, I included its license
here as LICENSE-RYU.txt. The Ryu code that
is relevant to what we use here is also replicated with only
minor compatibility changes in the src/test
folder.
There's some cases where an individual method was ported from
another permissively-licensed repo, such as
a_cbrt
from Stand-alone-junk
(Marc B. Reynolds' in-progress code), or probit
from
P.J. Acklam's site
(archived), and for most of these I
have tried to provide credit and a link to the source in the
documentation for that method. Some code is derived from
papers, like how Distributor uses
this algorithm by Paul Voutier.