Awesome
digital
Utilities for handling math and showing numbers in Java.
What is it?
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. 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. 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()
.
Also 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()
.
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,
and "smoother" sin/cos/tan versions that interpolate between
entries in the lookup table for very high precision. It should
be pointed out 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.
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. 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.
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
or PasarRandom
generators are similar to or the same as AlternateRandom's
algorithm, and offer 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, and code for replacing text with literals. 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.
How do I get it?
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.
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.5.4"
If you target GWT using libGDX, you will also need this in your
html module's build.gradle
:
api "com.github.tommyettinger:digital:0.5.4:sources"
GWT needs to be told about these changes in your GdxDefinition.gwt.xml
file. For digital 0.1.7 and later, use:
<inherits name="com.github.tommyettinger.digital" />
If you are using 0.1.6 or older, there are probably some GWT compatibility issues, though you can try using this. You should update to 0.1.7 or later instead of using this:
<inherits name="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.
License
Apache 2.0. This includes some modified code from
libGDX,
SquidLib,
SquidSquad,
Uncommon Maths,
wyhash,
Apache Commons Lang,
fastapprox,
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.