diff --git a/NEWS.md b/NEWS.md index 7a98f97ccc9c7..a3a8ef303a003 100644 --- a/NEWS.md +++ b/NEWS.md @@ -21,6 +21,7 @@ Language changes * The `hash(::AbstractString)` function is now a zero-copy / zero-cost function, based upon providing a correct implementation of the `codeunit` and `iterate` functions. Third-party string packages should migrate to the new algorithm by deleting their existing overrides of the `hash` function. ([#59691]) * Indexless `getindex` and `setindex!` (i.e. `A[]`) on `ReinterpretArray` now correctly throw a `BoundsError` when there is more than one element. ([#58814]) +* Characters can be parsed to more number types; `tryparse` now accepts characters as well as strings ([#59603]) Compiler/Runtime improvements ----------------------------- diff --git a/base/parse.jl b/base/parse.jl index 4309094e9fa1d..34d8f04d17855 100644 --- a/base/parse.jl +++ b/base/parse.jl @@ -5,9 +5,10 @@ import Base.Checked: add_with_overflow, mul_with_overflow ## string to integer functions ## """ - parse(type, str; base) + parse(type, str) + parse(<:Integer, str; base=10) -Parse a string as a number. For `Integer` types, a base can be specified +Parse a string (or character) as a number. For `Integer` types, a base can be specified (the default is 10). For floating-point types, the string is parsed as a decimal floating-point number. `Complex` types are parsed from decimal strings of the form `"R±Iim"` as a `Complex(R,I)` of the requested type; `"i"` or `"j"` can also be @@ -17,6 +18,9 @@ If the string does not contain a valid number, an error is raised. !!! compat "Julia 1.1" `parse(Bool, str)` requires at least Julia 1.1. +!!! compat "Julia 1.13" + `parse(type, AbstractChar)` requires at least Julia 1.13 for non-integer types. + # Examples ```jldoctest julia> parse(Int, "1234") @@ -38,16 +42,50 @@ julia> parse(Complex{Float64}, "3.2e-1 + 4.5im") parse(T::Type, str; base = Int) parse(::Type{Union{}}, slurp...; kwargs...) = error("cannot parse a value as Union{}") -function parse(::Type{T}, c::AbstractChar; base::Integer = 10) where T<:Integer - a::Int = (base <= 36 ? 10 : 36) - 2 <= base <= 62 || throw(ArgumentError("invalid base: base must be 2 ≤ base ≤ 62, got $base")) - d = '0' <= c <= '9' ? c-'0' : - 'A' <= c <= 'Z' ? c-'A'+10 : - 'a' <= c <= 'z' ? c-'a'+a : throw(ArgumentError("invalid digit: $(repr(c))")) - d < base || throw(ArgumentError("invalid base $base digit $(repr(c))")) - convert(T, d) +""" + tryparse(type, str) + tryparse(<:Integer, str; base=10) + +Like [`parse`](@ref), but returns either a value of the requested type, +or [`nothing`](@ref) if the string does not contain a valid number. + +!!! compat "Julia 1.13" + `tryparse(type, AbstractChar)` requires at least Julia 1.13. +""" +tryparse(T::Type, str; base = Int) + +@noinline function _invalid_base(base) + throw(ArgumentError("invalid base: base must be 2 ≤ base ≤ 62, got $base")) +end + +@noinline _invalid_digit(base, char) = throw(ArgumentError("invalid base $base digit $(repr(char))")) + +function parse_char(::Type{T}, c::AbstractChar, base::Integer, throw::Bool) where T + a::UInt8 = (base <= 36 ? 10 : 36) + (2 <= base <= 62) || _invalid_base(base) + base = base % UInt8 + u = reinterpret(UInt32, Char(c)::Char) + cp = u > 0x7a000000 ? 0xff : (u >> 24) % UInt8 + d = UInt8('0') ≤ cp ≤ UInt8('9') ? cp - UInt8('0') : + UInt8('A') ≤ cp ≤ UInt8('Z') ? cp - UInt8('A') + UInt8(10) : + UInt8('a') ≤ cp ≤ UInt8('z') ? cp - UInt8('a') + a : + 0xff + d < base || (throw ? _invalid_digit(base, c) : return nothing) + convert(T, d)::T end +function parse(::Type{T}, c::AbstractChar; base::Integer=10) where {T <: Integer} + @inline parse_char(T, c, base, true) +end + +function tryparse(::Type{T}, c::AbstractChar; base::Integer=10) where {T <: Integer} + @inline parse_char(T, c, base, false) +end + +# For consistency with parse(t, AbstractString), support a `base` argument only when T<:Integer +parse(::Type{T}, c::AbstractChar) where T = @inline parse_char(T, c, 10, true) +tryparse(::Type{T}, c::AbstractChar) where T = @inline parse_char(T, c, 10, false) + function parseint_iterate(s::AbstractString, startpos::Int, endpos::Int) (0 < startpos <= endpos) || (return Char(0), 0, 0) j = startpos @@ -116,8 +154,7 @@ function tryparse_internal(::Type{T}, s::AbstractString, startpos::Int, endpos:: return nothing end if !(2 <= base <= 62) - raise && throw(ArgumentError(LazyString("invalid base: base must be 2 ≤ base ≤ 62, got ", base))) - return nothing + raise ? _invalid_base(base) : return nothing end if i == 0 raise && throw(ArgumentError("premature end of integer: $(repr(SubString(s,startpos,endpos)))")) @@ -236,7 +273,7 @@ end if 2 <= base <= 62 return base end - throw(ArgumentError("invalid base: base must be 2 ≤ base ≤ 62, got $base")) + _invalid_base(base) end """ diff --git a/test/parse.jl b/test/parse.jl index e2b94a45cc446..22fb312f15662 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -29,6 +29,18 @@ @test parse(Int, 'a', base=16) == 10 @test_throws ArgumentError parse(Int, 'a') @test_throws ArgumentError parse(Int,typemax(Char)) + + @test tryparse(Int, '8') === 8 + @test tryparse(Int, 'a') === nothing + @test tryparse(Int, 'a'; base=11) === 10 + @test tryparse(Int32, 'a'; base=11) === Int32(10) + @test tryparse(UInt8, 'f'; base=16) === 0x0f + @test tryparse(UInt8, 'f'; base=15) === nothing + + @test_throws ArgumentError parse(Int, 'a'; base=63) + @test_throws ArgumentError tryparse(Int, 'a'; base=63) + @test_throws ArgumentError parse(Int, "a"; base=63) + @test_throws ArgumentError tryparse(Int, "a"; base=63) end # Issue 29451 @@ -265,6 +277,19 @@ end @test tryparse(Float32, "1.23") === 1.23f0 @test tryparse(Float16, "1.23") === Float16(1.23) +@testset "parse Chars to non-integer types" begin + for T in (Float64, Float32, Float16, Complex{Int}) + @test parse(T, '3') === T(3) + @test_throws ArgumentError parse(T, 'a') + @test tryparse(T, '3') === T(3) + @test tryparse(T, 'a') === nothing + + # for consistency with parse(T, str), `base` is not a valid argument + @test_throws MethodError parse(T, '3'; base=11) + @test_throws MethodError tryparse(T, '3'; base=11) + end +end + # parsing complex numbers (#22250) @testset "complex parsing" begin for sign in ('-','+'), Im in ("i","j","im"), s1 in (""," "), s2 in (""," "), s3 in (""," "), s4 in (""," ")