/digital

Utilities for handling math and showing numbers in Java.

Primary LanguageJavaApache License 2.0Apache-2.0

digital

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.