Partially revamp the HalfInteger type (#4)

* Call the HalfInteger field twofold

Makes it more immediately obvious what the meaning of the value stored
in the field is.

* Introduce new constructors for HalfInteger

The primary inner constructor mirrors the two-argument constructor of
the Rational type, where the user provides the numerator and denominator
values.

There is also a single argument outer constructor that makes HalfInteger
behave like a normal numeric type such that HalfInteger(n) == n.

* Move HalfInteger tests to a separate file

The using statements in halfinteger.jl are there so that it would be
possible to run the file separately from the other tests.

* Test the single-argument HalfInteger constructor

* Organize halfinteger.jl a bit

Prioritise the convert methods.

* Add multiplication with integer to HalfInteger

* Implement parsing and printing for HalfInteger

* parse(::HalfInteger, x) method
* Overload show to pretty-print HalfInteger

* Overload Base.numerator/denominator

And add tests for the other supplementary functions and methods as
well.

* Add HalfIntegerRange type

Can be constructed using the range operator :. Currently only supports
unit steps in the positive direction.

* Address feedback

* Rename .twofold -> .numerator
* Consistent variable names
* Remove unnecessary methods for HalfIntegerRange

* Allow constructing HalfIntegerRange with non-integer difference

* Add docs and ceil(::HalfInteger)
This commit is contained in:
Morten Piibeleht 2019-01-11 09:50:46 +13:00 committed by Jutho
parent 80038db6a3
commit 8ebb2c791b
4 changed files with 341 additions and 46 deletions

View file

@ -27,6 +27,9 @@ While the following function signatures are probably self-explanatory, you can q
* `δ(j₁, j₂, j₃) -> ::Bool`
* `Δ(T::Type{<:AbstractFloat} = Float64, j₁, j₂, j₃) -> ::T`
The package also defines the `HalfInteger` type that can be used to represent half-integer values.
Furthermore, the range operator `a:b` can be used to create ranges of `HalfInteger` values (a `HalfIntegerRange`).
## Implementation
Largely based on reading the paper (but not the code):

View file

@ -1,25 +1,72 @@
# HalfInteger
"""
struct HalfInteger <: Real
Represents half-integer values.
---
HalfInteger(numerator::Integer, denominator::Integer)
Constructs a `HalfInteger` object as a rational number from the given integer numerator
and denominator values.
# Examples
```jldoctest
julia> HalfInteger(1, 2)
1/2
julia> HalfInteger(-2, 1)
-2
```
"""
struct HalfInteger <: Real
num::Int
numerator::Int # with an implicit denominator of 2
function HalfInteger(num::Integer, den::Integer)
(den == 2) && return new(num)
(den == 1) && return new(2*num)
(den == 0) && throw(ArgumentError("Denominator can not be zero."))
# If non-trivial, we'll see if we can reduce it down to a half-integer
numerator, r = divrem(2*num, den)
if r == 0
return new(numerator)
else
throw(ArgumentError("$num // $den is not a half-integer value."))
end
end
end
Base.:+(a::HalfInteger, b::HalfInteger) = HalfInteger(a.num+b.num)
Base.:-(a::HalfInteger, b::HalfInteger) = HalfInteger(a.num-b.num)
Base.:-(a::HalfInteger) = HalfInteger(-a.num)
Base.:<=(a::HalfInteger, b::HalfInteger) = a.num <= b.num
Base.:<(a::HalfInteger, b::HalfInteger) = a.num < b.num
Base.one(::Type{HalfInteger}) = HalfInteger(2)
Base.zero(::Type{HalfInteger}) = HalfInteger(0)
"""
HalfInteger(x::Real)
Attempts to create a `HalfInteger` out of the real number `x`. Throws an `InexactError` if
`x` can not be represented as a half-integer value.
# Examples
```jldoctest
julia> HalfInteger(3)
3
julia> HalfInteger(1.5)
3/2
```
"""
HalfInteger(x::Real) = convert(HalfInteger, x)
Base.promote_rule(::Type{HalfInteger}, ::Type{<:Integer}) = HalfInteger
Base.promote_rule(::Type{HalfInteger}, T::Type{<:Rational}) = T
Base.promote_rule(::Type{HalfInteger}, T::Type{<:Real}) = T
Base.convert(::Type{HalfInteger}, n::Integer) = HalfInteger(2*n)
Base.convert(::Type{HalfInteger}, n::Integer) = HalfInteger(2*n, 2)
function Base.convert(::Type{HalfInteger}, r::Rational)
if r.den == 1
return HalfInteger(2*r.num)
return HalfInteger(2*r.num, 2)
elseif r.den == 2
return HalfInteger(r.num)
return HalfInteger(r.num, 2)
else
throw(InexactError(:HalfInteger, HalfInteger, r))
end
@ -27,19 +74,39 @@ end
function Base.convert(::Type{HalfInteger}, r::Real)
num = 2*r
if isinteger(num)
return HalfInteger(convert(Int, num))
return HalfInteger(convert(Int, num), 2)
else
throw(InexactError(:HalfInteger, HalfInteger, r))
end
end
Base.convert(T::Type{<:Integer}, s::HalfInteger) = iseven(s.num) ? convert(T, s.num>>1) : throw(InexactError(Symbol(T), T, s))
Base.convert(T::Type{<:Rational}, s::HalfInteger) = convert(T, s.num//2)
Base.convert(T::Type{<:Real}, s::HalfInteger) = convert(T, s.num/2)
Base.convert(T::Type{<:Integer}, s::HalfInteger) = iseven(s.numerator) ? convert(T, s.numerator>>1) : throw(InexactError(Symbol(T), T, s))
Base.convert(T::Type{<:Rational}, s::HalfInteger) = convert(T, s.numerator//2)
Base.convert(T::Type{<:Real}, s::HalfInteger) = convert(T, s.numerator/2)
Base.convert(::Type{HalfInteger}, s::HalfInteger) = s
# Arithmetic
Base.:+(a::HalfInteger, b::HalfInteger) = HalfInteger(a.numerator+b.numerator, 2)
Base.:-(a::HalfInteger, b::HalfInteger) = HalfInteger(a.numerator-b.numerator, 2)
Base.:-(a::HalfInteger) = HalfInteger(-a.numerator, 2)
Base.:*(a::Integer, b::HalfInteger) = HalfInteger(a * b.numerator, 2)
Base.:*(a::HalfInteger, b::Integer) = b * a
Base.:<=(a::HalfInteger, b::HalfInteger) = a.numerator <= b.numerator
Base.:<(a::HalfInteger, b::HalfInteger) = a.numerator < b.numerator
Base.one(::Type{HalfInteger}) = HalfInteger(2, 2)
Base.zero(::Type{HalfInteger}) = HalfInteger(0, 2)
Base.floor(x::HalfInteger) = isinteger(x) ? x : x - HalfInteger(1, 2)
Base.floor(::Type{T}, x::HalfInteger) where T <: Integer = convert(T, floor(x))
Base.ceil(x::HalfInteger) = isinteger(x) ? x : x + HalfInteger(1, 2)
Base.ceil(::Type{T}, x::HalfInteger) where T <: Integer = convert(T, ceil(x))
# Hashing
function Base.hash(a::HalfInteger, h::UInt)
iseven(a.num) && return hash(a.num>>1, h)
num, den = a.num, 2
iseven(a.numerator) && return hash(a.numerator>>1, h)
num, den = a.numerator, 2
den = 1
pow = -1
if abs(num) < 9007199254740992
@ -51,10 +118,73 @@ function Base.hash(a::HalfInteger, h::UInt)
return h
end
Base.isinteger(a::HalfInteger) = iseven(a.num)
# Parsing and printing
"""
parse(HalfInteger, s)
Parses the string `s` into the corresponding `HalfInteger`-value. String can either be a
number or a fraction of the form `n/2`.
"""
function Base.parse(::Type{HalfInteger}, s::AbstractString)
if in('/', s)
num, den = split(s, '/'; limit=2)
parse(Int, den) == 2 ||
throw(ArgumentError("Denominator not 2 in HalfInteger string '$s'."))
HalfInteger(parse(Int, num), 2)
elseif !isempty(strip(s))
HalfInteger(parse(Int, s))
else
throw(ArgumentError("input string is empty or only contains whitespace"))
end
end
Base.show(io::IO, x::HalfInteger) =
print(io, iseven(x.numerator) ? "$(div(x.numerator, 2))" : "$(x.numerator)/2")
# Other methods
Base.isinteger(a::HalfInteger) = iseven(a.numerator)
ishalfinteger(a::HalfInteger) = true
ishalfinteger(a::Integer) = true
ishalfinteger(a::Rational) = a.den == 1 || a.den == 2
ishalfinteger(a::Real) = isinteger(2*a)
converthalfinteger(a::Number) = convert(HalfInteger, a)
Base.numerator(a::HalfInteger) = iseven(a.numerator) ? div(a.numerator, 2) : a.numerator
Base.denominator(a::HalfInteger) = iseven(a.numerator) ? 1 : 2
# Range of HalfIntegers
"""
struct HalfIntegerRange <: AbstractVector{HalfInteger}
A range of `HalfInteger` values from `start` to `stop`, spaced by `1`. The `a:b` syntax
where both `a` and `b` are `HalfInteger`s can also be use to construct this range.
"""
struct HalfIntegerRange <: AbstractVector{HalfInteger}
start :: HalfInteger
stop :: HalfInteger
function HalfIntegerRange(start::HalfInteger, stop::HalfInteger)
(start <= stop) ||
throw(ArgumentError("Second argument must be greater or equal to the first."))
return new(start, stop)
end
end
Base.iterate(it::HalfIntegerRange) = (it.start, it.start + 1)
Base.iterate(it::HalfIntegerRange, s) = (s <= it.stop) ? (s, s+1) : nothing
Base.length(it::HalfIntegerRange) = floor(Int, it.stop - it.start) + 1
Base.size(it::HalfIntegerRange) = (length(it),)
function Base.getindex(it::HalfIntegerRange, i::Integer)
1 <= i <= length(it) || throw(BoundsError(it, i))
it.start + i - 1
end
"""
(:)(i::HalfInteger, j::HalfInteger)
Constructs a `HalfIntegerRange` out of two `HalfInteger` values.
"""
Base.:(:)(i::HalfInteger, j::HalfInteger) = HalfIntegerRange(i, j)

188
test/halfinteger.jl Normal file
View file

@ -0,0 +1,188 @@
using Test
using WignerSymbols: HalfInteger, ishalfinteger, HalfIntegerRange
@testset "HalfInteger" begin
@testset "HalfInteger type" begin
# HalfInteger constructors
@test HalfInteger(1, 2).numerator == 1
@test HalfInteger(1, 1).numerator == 2
@test HalfInteger(0, 1).numerator == 0
@test HalfInteger(0, 2).numerator == 0
@test HalfInteger(0, 5).numerator == 0
@test HalfInteger(10, 5).numerator == 4
@test HalfInteger(21, 14).numerator == 3
@test HalfInteger(-3, 2).numerator == -3
@test HalfInteger(3, -2).numerator == -3
@test HalfInteger(-3, -2).numerator == 3
@test_throws ArgumentError HalfInteger(1, 0)
@test_throws ArgumentError HalfInteger(1, 3)
@test_throws ArgumentError HalfInteger(1, -3)
@test_throws ArgumentError HalfInteger(-5, 3)
@test_throws ArgumentError HalfInteger(-1000, -999)
# convert methods
@test convert(HalfInteger, 2) == HalfInteger(2, 1)
@test convert(HalfInteger, 1//2) == HalfInteger(1, 2)
@test convert(HalfInteger, 1.5) == HalfInteger(3, 2)
@test_throws InexactError convert(HalfInteger, 1//3)
@test_throws InexactError convert(HalfInteger, 0.6)
@test convert(HalfInteger, 2) == 2
@test convert(HalfInteger, 1//2) == 1//2
@test convert(HalfInteger, 1.5) == 1.5
@test_throws InexactError convert(Integer, HalfInteger(1, 2))
# single-argument constructor
@test HalfInteger(0) == HalfInteger(0, 2)
@test HalfInteger(1) == HalfInteger(1, 1)
@test HalfInteger(2) == HalfInteger(2, 1)
@test HalfInteger(-30) == HalfInteger(-60, 2)
@test HalfInteger(0//2) == HalfInteger(0, 1)
@test HalfInteger(1//2) == HalfInteger(1, 2)
@test HalfInteger(-5//2) == HalfInteger(-5, 2)
end
a = HalfInteger(2)
b = HalfInteger(3, 2)
@testset "HalfInteger arithmetic" begin
@test a + b == 2 + 3//2
@test a - b == 2 - 3//2
@test zero(a) == 0
@test one(a) == 1
@test a > b
@test b < a
@test b <= a
@test a >= b
@test a == a
@test a != b
@test 2 * HalfInteger(0) == HalfInteger(0)
@test 2 * HalfInteger(1, 2) == HalfInteger(1)
@test HalfInteger(1) * 2 == HalfInteger(2)
@test 2 * a == HalfInteger(4)
@test (-1) * b == HalfInteger(-3//2)
@test floor(HalfInteger(0)) === HalfInteger(0)
@test floor(HalfInteger(-1)) === HalfInteger(-1)
@test floor(HalfInteger(1, 2)) === HalfInteger(0)
@test floor(HalfInteger(-1, 2)) === HalfInteger(-1)
@test floor(Int, HalfInteger(0)) === 0
@test floor(Int, HalfInteger(1, 2)) === 0
@test floor(Int32, HalfInteger(-5, 2)) === Int32(-3)
@test floor(Int32, HalfInteger(5)) === Int32(5)
@test ceil(HalfInteger(0)) === HalfInteger(0)
@test ceil(HalfInteger(-1)) === HalfInteger(-1)
@test ceil(HalfInteger(1, 2)) === HalfInteger(1)
@test ceil(HalfInteger(-1, 2)) === HalfInteger(0)
@test ceil(Int, HalfInteger(0)) === 0
@test ceil(Int, HalfInteger(1, 2)) === 1
@test ceil(Int32, HalfInteger(-5, 2)) === Int32(-2)
@test ceil(Int32, HalfInteger(5)) === Int32(5)
for n in -98:7:98
halfint, rat = HalfInteger(n, 2), n // 2
@test halfint == rat
@test halfint == HalfInteger(n / 2)
iseven(n) && @test halfint == HalfInteger(div(n, 2))
@test ceil(halfint) == ceil(rat)
@test floor(halfint) == floor(rat)
end
end
@testset "Parsing and printing" begin
@test string(HalfInteger(0)) == "0"
@test string(HalfInteger(1)) == "1"
@test string(HalfInteger(-1)) == "-1"
@test string(HalfInteger(1, 2)) == "1/2"
@test string(HalfInteger(-3, 2)) == "-3/2"
@test parse(HalfInteger, "0") == HalfInteger(0)
@test parse(HalfInteger, "1") == HalfInteger(1)
@test parse(HalfInteger, "210938") == HalfInteger(210938)
@test parse(HalfInteger, "-15") == HalfInteger(-15)
@test parse(HalfInteger, "1/2") == HalfInteger(1//2)
@test parse(HalfInteger, "-3/2") == HalfInteger(-3//2)
@test_throws ArgumentError parse(HalfInteger, "")
@test_throws ArgumentError parse(HalfInteger, "-50/100")
@test_throws ArgumentError parse(HalfInteger, "1/3")
end
@testset "HalfInteger hashing" begin
@test hash(a) == hash(2)
@test hash(b) == hash(1.5)
end
@testset "Other HalfInteger methods" begin
@test isinteger(HalfInteger(0))
@test isinteger(HalfInteger(1))
@test !isinteger(HalfInteger(1, 2))
@test ishalfinteger(1)
@test ishalfinteger(1.0)
@test ishalfinteger(-0.5)
@test ishalfinteger(HalfInteger(0))
@test ishalfinteger(HalfInteger(1, 2))
@test ishalfinteger(1//1)
@test ishalfinteger(1//2)
@test !ishalfinteger(0.3)
@test !ishalfinteger(-5//7)
@test numerator(HalfInteger(0)) == 0
@test numerator(HalfInteger(1, 2)) == 1
@test numerator(HalfInteger(1)) == 1
@test numerator(HalfInteger(-3, 2)) == -3
@test denominator(HalfInteger(0)) == 1
@test denominator(HalfInteger(1, 2)) == 2
@test denominator(HalfInteger(1)) == 1
@test denominator(HalfInteger(-3, 2)) == 2
end
@testset "HalfIntegerRange" begin
hi(x) = HalfInteger(x)
@test length(HalfIntegerRange(hi(0), hi(0))) == 1
@test length(HalfIntegerRange(hi(0), hi(2))) == 3
let hirange = HalfIntegerRange(hi(-1//2), hi(1//2))
@test length(hirange) == 2
@test size(hirange) == (2,)
@test collect(hirange) == [hi(-1//2), hi(1//2)]
end
let hirange = HalfIntegerRange(hi(0), hi(1//2))
@test length(hirange) == 1
@test size(hirange) == (1,)
@test collect(hirange) == [hi(0)]
end
let hirange = HalfIntegerRange(hi(1//2), hi(3))
@test length(hirange) == 3
@test size(hirange) == (3,)
@test collect(hirange) == [hi(1//2), hi(3//2), hi(5//2)]
end
@test hi(5):hi(7) == HalfIntegerRange(hi(5), hi(7))
@test hi(-1//2):hi(1//2) == HalfIntegerRange(hi(-1//2), hi(1//2))
@test collect(hi(0) : hi(2)) == [hi(0), hi(1), hi(2)]
@test collect(hi(-3//2) : hi(1//2)) == [hi(-3//2), hi(-1//2), hi(1//2)]
let hirange = hi(-3//2):hi(0)
@test length(hirange) == 2
@test size(hirange) == (2,)
@test collect(hirange) == [hi(-3//2), hi(-1//2)]
end
@test hi(1//2) hi(-1//2) : hi(1//2)
@test 1 hi(0) : hi(2)
@test 1//2 hi(-1//2) : hi(7//2)
@test !(hi(1//2) hi(0) : hi(1))
@test !(1//2 hi(-1) : hi(7))
r = hi(-3//2) : hi(3//2)
@test r[1] == hi(-3//2)
@test r[2] == hi(-1//2)
@test r[3] == hi(1//2)
@test r[4] == hi(3//2)
@test_throws BoundsError r[0]
@test_throws BoundsError r[5]
end
end

View file

@ -2,37 +2,11 @@ using Test
using WignerSymbols
using LinearAlgebra
using WignerSymbols: HalfInteger
include("halfinteger.jl")
smalljlist = 0:1//2:10
largejlist = 0:1//2:1000
@testset "HalfInteger" begin
@test convert(HalfInteger, 2) == HalfInteger(4)
@test convert(HalfInteger, 1//2) == HalfInteger(1)
@test convert(HalfInteger, 1.5) == HalfInteger(3)
@test_throws InexactError convert(HalfInteger, 1//3)
@test_throws InexactError convert(HalfInteger, 0.6)
@test convert(HalfInteger, 2) == 2
@test convert(HalfInteger, 1//2) == 1//2
@test convert(HalfInteger, 1.5) == 1.5
@test_throws InexactError convert(Integer, HalfInteger(1))
a = HalfInteger(4)
b = HalfInteger(3)
@test a + b == 2 + 3//2
@test a - b == 2 - 3//2
@test zero(a) == 0
@test one(a) == 1
@test a > b
@test b < a
@test b <= a
@test a >= b
@test a == a
@test a != b
@test hash(a) == hash(2)
@test hash(b) == hash(1.5)
@test hash(b) == hash(3//2)
end
@testset "triangle coefficient" begin
for j1 in smalljlist, j2 in smalljlist
for j3 = abs(j1-j2):(j1+j2)