From 8ebb2c791ba9c634532794f664e507308128c095 Mon Sep 17 00:00:00 2001 From: Morten Piibeleht Date: Fri, 11 Jan 2019 09:50:46 +1300 Subject: [PATCH] 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) --- README.md | 3 + src/halfinteger.jl | 166 +++++++++++++++++++++++++++++++++----- test/halfinteger.jl | 188 ++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 30 +------ 4 files changed, 341 insertions(+), 46 deletions(-) create mode 100644 test/halfinteger.jl diff --git a/README.md b/README.md index 0e97ab7..3a4477a 100644 --- a/README.md +++ b/README.md @@ -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): diff --git a/src/halfinteger.jl b/src/halfinteger.jl index 00b728a..fb98314 100644 --- a/src/halfinteger.jl +++ b/src/halfinteger.jl @@ -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) diff --git a/test/halfinteger.jl b/test/halfinteger.jl new file mode 100644 index 0000000..3d30d65 --- /dev/null +++ b/test/halfinteger.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 9e6408d..58de51f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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)