diff --git a/docs/src/index.md b/docs/src/index.md index 56c9abf..434c30c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -140,3 +140,19 @@ Helper macro for returning from the enclosing block when there are no more eleme ```@docs IterTools.@ifsomething ``` + +## properties(x) + +Iterate over struct or named tuple properties. + +```@docs +properties +``` + +## propertyvalues(x) + +Iterate over struct or named tuple property values. + +```@docs +propertyvalues +``` diff --git a/src/IterTools.jl b/src/IterTools.jl index 6a2d1c0..478f3c5 100644 --- a/src/IterTools.jl +++ b/src/IterTools.jl @@ -26,7 +26,9 @@ export ncycle, ivec, flagfirst, - takewhile + takewhile, + properties, + propertyvalues function has_length(it) it_size = IteratorSize(it) @@ -908,4 +910,67 @@ Base.IteratorSize(it::TakeWhile) = Base.SizeUnknown() eltype(::Type{TakeWhile{I}}) where {I} = eltype(I) IteratorEltype(::Type{TakeWhile{I}}) where {I} = IteratorEltype(I) +struct Properties{T} + x::T + n::Int + names +end + +""" + properties(x) + +Iterate through the names and value of the properties of `x`. + +```jldoctest +julia> collect(properties(1 + 2im)) +2-element Array{Any,1}: + (:re, 1) + (:im, 2) +``` +""" +function properties(x::T) where T + names = propertynames(x) + return Properties{T}(x, length(names), names) +end + +function iterate(p::Properties, state=1) + state > length(p) && return nothing + + name = p.names[state] + return ((name, getproperty(p.x, name)), state + 1) +end + +struct PropertyValues{T} + x::T + n::Int + names +end + +""" + propertyvalues(x) + +Iterate through the values of the properties of `x`. + +```jldoctest +julia> collect(propertyvalues(1 + 2im)) +2-element Array{Any,1}: + 1 + 2 +``` +""" +function propertyvalues(x::T) where T + names = propertynames(x) + return PropertyValues{T}(x, length(names), names) +end + +function iterate(p::PropertyValues, state=1) + state > length(p) && return nothing + + name = p.names[state] + return (getproperty(p.x, name), state + 1) +end + +length(p::Union{Properties, PropertyValues}) = p.n +IteratorSize(::Type{<:Union{Properties, PropertyValues}}) = HasLength() + end # module IterTools diff --git a/test/runtests.jl b/test/runtests.jl index 26f04b1..3ce8d9a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -407,12 +407,53 @@ include("testing_macros.jl") @test collect(flagfirst(Int[])) == Tuple{Bool,Int}[] end - + @testset "takewhile" begin @test collect(takewhile(x -> x^2 < 10, 1:10)) == Any[1, 2, 3] @test collect(takewhile(x -> x^2 < 10, Iterators.countfrom(1))) == Any[1, 2, 3] @test collect(takewhile(x -> x^2 < 10, 5:10)) == Any[] @test collect(takewhile(x -> true, 5:10)) == collect(5:10) end + + @testset "properties" begin + p1 = properties(1 + 2im) + @test IteratorEltype(p1) isa HasEltype + @test eltype(p1) == Any + @test IteratorSize(p1) isa HasLength + @test length(p1) == 2 + @test collect(p1) == Any[(:re, 1), (:im, 2)] + + ntp = (a = "", b = 1, c = 2.0) + p2 = properties(ntp) + @test collect(p2) == Tuple.(collect(pairs(ntp))) + + # HasLength used as an example no-field struct + p3 = properties(HasLength()) + @test collect(p3) == Any[] + end + + @testset "propertyvalues" begin + pv1 = propertyvalues(1 + 2im) + @test IteratorEltype(pv1) isa HasEltype + @test eltype(pv1) == Any + @test IteratorSize(pv1) isa HasLength + @test length(pv1) == 2 + @test collect(pv1) == Any[1, 2] + + tp = ("", 1, 2.0) + pv2 = propertyvalues(tp) + + # getproperty for tuples wasn't introduced until 1.2 + # https://github.com/JuliaLang/julia/pull/31324 + @static if VERSION < v"1.2.0-DEV.460" + @test_broken collect(pv2) == collect(tp) + else + @test collect(pv2) == collect(tp) + end + + # HasLength used as an example no-field struct + pv3 = propertyvalues(HasLength()) + @test collect(pv3) == Any[] + end end end