Manual

Half-integer types

Note

In this package, any number $\frac{n}{2}$ where $n\in\mathbb{Z}$ is considered a half-integer – contrary to the common definition, $n$ does not have to be odd, i.e., the integers are a subset of the half-integers.

The abstract type HalfInteger <: Real is provided as a supertype for all half-integer types. Concrete half-integer types are provided in the form of the parametric type Half{T} <: HalfInteger where T can be any Integer type. The type Half{T} can represent any number n/2 where n is of type T.

julia> Half{Int}(5//2)
5/2

julia> Half{Int8}(3)
3

julia> ans isa HalfInteger
true

julia> typemin(Half{Int8}), typemax(Half{Int8})
(-64, 127/2)

For convenient use, aliases for Half{T} exist for all standard integer types T defined in Julia (except for Bool). For example, HalfInt can be used instead of Half{Int}:

julia> HalfInt(3.5)
7/2

julia> typeof(ans) # Half{Int32} or Half{Int64} depending on on system word size
Half{Int64}

The following aliases are defined:

TAlias for Half{T}
IntHalfInt
Int8HalfInt8
Int16HalfInt16
Int32HalfInt32
Int64HalfInt64
Int128HalfInt128
BigIntBigHalfInt (notHalfBigInt!)
UIntHalfUInt
UInt8HalfUInt8
UInt16HalfUInt16
UInt32HalfUInt32
UInt64HalfUInt64
UInt128HalfUInt128

Construction of HalfIntegers

HalfIntegers can be created from any other number type using constructors or convert:

julia> HalfUInt(5.5)
11/2

julia> convert(BigHalfInt, 3//2)
3/2

julia> convert(Complex{HalfInt}, 2.5 + 3.5im)
5/2 + 7/2*im

Alternatively, one can use the half function, which halves its argument and returns an appropriate HalfInteger or Complex{<:HalfInteger} type:

julia> half(3)
3/2

julia> half(4.0)
2

julia> half(3 - 4im)
3/2 - 2*im

Note that the argument must be an integer value (or a complex value with integer real and imaginary parts), but does not need to be of an Integer or Complex{<:Integer} type. An optional argument can be used to specify the return type, which must be <:HalfInteger or <:Complex{<:HalfInteger}:

julia> half(BigHalfInt, -11)
-11/2

julia> half(Complex{HalfInt}, 4+1im)
2 + 1/2*im

Calling the Half constructor without type parameter is disallowed to avoid confusion with the half function.

Conversion

HalfIntegers can be converted to any other number type in the usual ways. When the value is not representable in the new type, an error is thrown:

julia> Float32(HalfInt(3/2))
1.5f0

julia> convert(Rational{Int}, HalfInt(-5))
-5//1

julia> float(HalfInt(2))
2.0

julia> complex(HalfInt(-1/2))
-1/2 + 0*im

julia> convert(Int, HalfInt(7/2))
ERROR: InexactError: Int64(7/2)
Note

The fastest way of converting a HalfInteger to an Integer type is the floor function, since it reduces to a single bit-shift:

julia> floor(Integer, HalfInt(5)) # returns an Int
5

Thus, convert(Integer, x) can be replaced by floor(Integer, x) when it is known that x is equal to an integer. This can be useful in performance-critical applications.

Arithmetic operations

The provided half-integer types support all common arithmetic operations. For operations between different types, the values are promoted to an appropriate type:

julia> HalfInt(1/2) + HalfInt(5)
11/2

julia> HalfInt(1/2) + 5
11/2

julia> HalfInt(1/2) + 5.0
5.5

julia> complex(HalfInt(1/2)) + 5//1
11//2 + 0//1*im

Since the product of two half-integers is not a half-integer (unless one of them is actually an integer), multiplication of two HalfIntegers results in a floating-point number. Multiplication of a HalfInteger and an Integer yields another HalfInteger:

julia> HalfInt(1/2) * HalfInt(7/2)
1.75

julia> HalfInt(1/2) * HalfInt(5)
2.5

julia> HalfInt(1/2) * 5
5/2

Dividing two half-integers result in a floating-point number as well, but rational and Euclidean division are defined as well:

julia> HalfInt(7/2) / HalfInt(3/2)
2.3333333333333335

julia> HalfInt(7/2) // HalfInt(3/2)
7//3

julia> HalfInt(7/2) ÷ HalfInt(3/2)
2
Note

The HalfInteger type aims to support every operation that is implemented for Rationals in Base. Some operations are only available in newer Julia versions. For example, gcd for rational numbers is only defined in Julia 1.4 or newer, and is therefore only extended to HalfIntegers in those Julia versions.

Auxiliary functions

twice

The twice function can be regarded as the inverse of the half function: it doubles its argument. However, in contrast to half, which always returns a HalfInteger or Complex{<:HalfInteger}, twice only returns an Integer when the argument is a HalfInteger or an Integer. Alternatively, the return type of twice can be specified with an optional first argument:

julia> twice(HalfInt32(5/2)) # returns an Int32
5

julia> twice(3.5)            # returns a Float64
7.0

julia> twice(Integer, 3.5)   # returns an Int
7

Furthermore, while half only accepts integer values (or complex values with integer components), the argument of twice may have any numeric value:

julia> twice(3//8)
3//4

julia> half(ans)
ERROR: InexactError: Integer(3//4)

onehalf

Analogous to zero and one, the function onehalf returns the value 1/2 in the specified type (the argument may be a value of the desired type or the type itself):

julia> onehalf(HalfInt)
1/2

julia> onehalf(big"2.0")
0.50

julia> onehalf(7//3)
1//2

ishalfinteger

The function ishalfinteger can be used to check whether a number is equal to some half-integer:

julia> ishalfinteger(0.5)
true

julia> ishalfinteger(4)
true

julia> ishalfinteger(1//3)
false

Wraparound behavior

Since the implementation of the HalfInteger type is based on the underlying integers (e.g., standard Int arithmetic in the case of the HalfInt type), HalfIntegers may be subject to integer overflow:

julia> typemax(HalfInt64)
9223372036854775807/2

julia> ans + onehalf(HalfInt64)
-4611686018427387904

The behavior in the above example is due to 9223372036854775807 + 1 == -9223372036854775808.

Overflows can also occur when converting an Integer to a HalfInteger type (which may happen implicitly due to promotion):

julia> typemax(Int64)
9223372036854775807

julia> HalfInt64(ans)  # 2 * 9223372036854775807 == -2
-1

If you prefer checked arithmetic, you can use the SaferIntegers package:

julia> using SaferIntegers

julia> const SafeHalfInt64 = Half{SafeInt64}
Half{SafeInt64}

julia> typemax(SafeHalfInt64)
9223372036854775807/2

julia> ans + onehalf(SafeHalfInt64)
ERROR: OverflowError: 9223372036854775807 + 1 overflowed for type Int64
Compat

Due to a bug, the Half{SafeInt8}, Half{SafeInt16}, Half{SafeUInt8} and Half{SafeUInt16} types require Julia ≥ 1.1 to work correctly.

Custom HalfInteger types

To implement a custom type MyHalfInt <: HalfInteger, at least the following methods must be defined:

Required methodBrief description
half(::Type{MyHalfInt}, x)Return x/2 as a value of type MyHalfInt
twice(x::MyHalfInt)Return 2x as a value of some Integer type

Thus, a simple implementation of a custom HalfInteger type may look like this:

struct MyHalfInt <: HalfInteger
    val::HalfInt
end

MyHalfInt(x::MyHalfInt) = x  # to avoid method ambiguity

HalfIntegers.half(::Type{MyHalfInt}, x) = MyHalfInt(half(HalfInt, x))

HalfIntegers.twice(x::MyHalfInt) = twice(x.val)

The MyHalfInt type supports all arithmetic operations defined for HalfIntegers. However, some operations will return values of a Half{T} type, which may not be desirable:

julia> MyHalfInt(3/2) + MyHalfInt(2)
7/2

julia> typeof(ans)
Half{Int64}

The following sections describe how to customize this behavior.

Customizing arithmetic operators and functions

The operators/functions that return a Half{T} type are +, -, mod and rem (and other functions that make use of those, like round). If we want those operations to return MyHalfInts, we can define the following methods (note that both one- and two-argument versions of + and - are defined):

Base.:+(x::MyHalfInt) = x
Base.:+(x::MyHalfInt, y::MyHalfInt) = MyHalfInt(x.val + y.val)

Base.:-(x::MyHalfInt) = MyHalfInt(-x.val)
Base.:-(x::MyHalfInt, y::MyHalfInt) = MyHalfInt(x.val - y.val)

Base.mod(x::MyHalfInt, y::MyHalfInt) = MyHalfInt(mod(x.val, y.val))
Base.rem(x::MyHalfInt, y::MyHalfInt) = MyHalfInt(rem(x.val, y.val))

Now, these arithmetic operations will return a MyHalfInt for MyHalfInt arguments. Certain operations will still yield other types:

  • * and / return floating-point numbers,
  • div, fld, cld etc. return values of some Integer type.

To change this behavior, we would need to define methods for these functions as well. For example, if we want our MyHalfInt type to return a Rational for multiplication and division, we could define the following methods:

Base.:*(x::MyHalfInt, y::MyHalfInt) = twice(x)*twice(y)//4
Base.:/(x::MyHalfInt, y::MyHalfInt) = twice(x)//twice(y)

Promotion rules

In order to make mixed-type operations work with our MyHalfInt type, promotion rules need to be defined. As a simple example, we can define our MyHalfInt type to promote like HalfInt as follows:

Base.promote_rule(::Type{MyHalfInt}, T::Type{<:Real}) = promote_type(HalfInt, T)

For more information on how to define promotion rules, cf. the Julia documentation.

Ranges of custom HalfInteger types

Ranges of custom HalfInteger types should work if either custom arithmetics or promotion rules are defined. However, intersecting these ranges may again yield ranges of Half{T} values:

julia> a = MyHalfInt(3/2):MyHalfInt(3/2):MyHalfInt(15/2)
3/2:3/2:15/2

julia> b = MyHalfInt(2):MyHalfInt(1):MyHalfInt(9)
2:1:9

julia> intersect(a, b)
3:3:6

julia> typeof(ans)
StepRange{Half{Int64},Half{Int64}}

In order to change this behavior, custom methods for Base.intersect need to be defined as well.