Skip to content

Loading…

RFC: NamedIndex and NamedVector types #1190

Closed
wants to merge 17 commits into from

6 participants

@HarlanH
The Julia Language member

These are relatively simple data types that are useful for data structures that are worked with in the REPL. NamedIndex is a mapping from either names (strings) or integer indexes to integer indexes. NamedVector wraps that in a type that also includes an arbitrary vector. These structures also include grouped names, which refer to groups of other names, and can be used transparently. The majority of this code is by Tom Short @tshort. NamedIndex is used by JuliaData.

NamedVector is missing most functionality but the basics, but thought I'd get comments before writing that code.

Simple example of NamedVector (see also the test suite):

julia> nams = ["one", "two", "three", "four"]
4-element ASCIIString Array:
 "one"  
 "two"  
 "three"
 "four" 

julia> vals = [11, 22, 33, 44]
4-element Int64 Array:
 11
 22
 33
 44

julia> t1 = NamedVector() # need constructors other than the default empty constructor!


julia> for i = 1:4
           t1[nams[i]] = vals[i]
       end

julia> print(t1)
1, one: 11
2, two: 22
3, three: 33
4, four: 44

julia> t1["one"]
11

julia> t1[2]
22

julia> t1[1:3]
3-element Any Array:
 11
 22
 33

julia> t1[["four", "two"]]
2-element Any Array:
 44
 22
@HarlanH HarlanH NamedIndex and NamedVector types
initial commit, including simple test suite:

```julia
load("named.jl")
load("test.jl")
tests("test/test_named.jl")
```
0a9af88
@StefanKarpinski
The Julia Language member

Love it.

@StefanKarpinski
The Julia Language member

I'm completely down to merge this, but I'm holding off because of the RFC subject line. Whenever you're good to go, Harlan, this is in. I've always wanted this kind of thing. Very handy.

@toivoh
The Julia Language member

Where does the mapping from name to index come from? Insertion order?

@HarlanH
The Julia Language member

@toivoh , yes, mostly. Typical use cases are setting all the names at once, replacing a name, or appending a new index. For the applications we've used this for (DataFrame and a project I'm doing for work), inserting in the middle hasn't come up.

@timholy
The Julia Language member

Looks nice. I presume the main extra functionality this adds over Dict is the ability to access elements either by index or name?

Very minor suggestion: you can avoid two Dict lookups, the first for checking to see if it has a name and the second to find the index associated with it, using code like this (cribbed from the options code):

index = Base.ht_keyindex(o.key2index,s)
    if index > 0
...
@HarlanH
The Julia Language member

@timholy Yes, that's right. It has fast access by name (via a Dict) and even faster access by index. It just passes integers (and Ranges, and Boolean vectors...) through to the usual array referencing code. Thanks for the reminder on ht_keyindex()! I'll take a look and see where that can be used.

@JeffBezanson
The Julia Language member

Why is NamedIndex mutable? That strikes me as very strange.

The Julia Language member

The names are mutable because when this is used with DataFrames (or similar), you often get default names from csvDataTable like X1, X2, X3..., and want to interactively change them to something useful. Or are you referring to something else?

The Julia Language member

Changing which names are associated with a data set is not necessarily the same as mutating the name mapping. You only need mutation if a NamedIndex might be shared among many data sets, and you want to change how they are all accessed by changing the one NamedIndex object.

@JeffBezanson
The Julia Language member

Copying all these with the extra layer of [ ...] to try to get a sharper-typed array is suspicious. Why not represent groups as vectors of strings; then NamedIndex could focus on handling only name to index mapping.

The Julia Language member

That could work. @tshort, you wrote this part -- what do you think?

The Julia Language member

Suspicious is probably a good term for that code bit. The implementation of group indexing is a little kludgy; it was bolted on after the fact because it was relatively easy to do. Group indexing is where a["group1"] might be equivalent to a[ [3,5] ]. Index deletion is pretty uncommon (at least for DataFrames), so I wasn't particularly worried about elegance or efficiency for this part.

@JeffBezanson, could you expand more on "vectors of strings"? I don't see what you are getting at..

The Julia Language member

I meant just having a variable like group1 = ["a","b"]. Selecting indexes should be separate from naming them. Also certain things depend on whether indexes are scalars, so having an index naming layer where each index may or may not be a scalar seems not to fit. Imagine what it would be like to use an array where some indexes referred to multiple elements. Simply iterating over it would suddenly be difficult.

@JeffBezanson
The Julia Language member

Automatic renaming doesn't seem to be a good programming interface. How does a client know their key got renamed? How do you even begin to use a dictionary that might change your keys?

The Julia Language member

Yeah, this is a bit dangerous. But necessary. It's used to generate new names if there are collisions after a relational join operation of DataFrames, for example. In general, I think it's best not to think of names as keys. The underlying representation is an array -- the names are just labels that you can use instead if it's more convenient or readable.

The Julia Language member

Ok, fair enough, if the use style is not to access things by name programmatically after certain operations.

@HarlanH
The Julia Language member

Jeff/Tom, I pushed some changes here that pull the implementation of groups out into a separate Dict, as suggested. Seems to work fine. I want to do one more thing on this before it could be merged in, which is make groups work for NamedVectors.

HarlanH added some commits
@HarlanH HarlanH better show(NamedVector) and show(NamedIndex) 37b2c56
@HarlanH HarlanH values(NamedVector) and find(NamedVector, value)
values() gives the underlying array values
find() gives the position of the value in the array, or 0
f372e2f
@tshort
The Julia Language member

I would make NamedVector inherit from AbstractVector instead of Associative. I think you want it to act like both, but I think it's better to get the numeric support (sum, mean, etc.) by inheriting from AbstractVector. You can get all of the Associative behaviors you want by defining appropriate methods. with and within might be the only tricky ones, but it's mostly copy and paste.

@StefanKarpinski
The Julia Language member

This is why we definitely need multiple inheritance. Huge language change though.

@JeffBezanson
The Julia Language member

I don't think something can be both a Vector and Associative, the way we've defined them so far. A vector implicitly has consecutive integers as keys, and an Associative has arbitrary keys. Iterating an Associative yields (key,value) tuples, and a vector just gives values.

@HarlanH
The Julia Language member
@tshort
The Julia Language member

It can still inherit from an AbstractVector and have iteration defined like an Associative type. I'm not sure whether Vector or Associative iteration is best in this case. The main reason for inheriting from an AbstractVector is so that sum(nv) works and nv1 .* nv2 works. I'd probably lean towards vector-style iteration in this case to make such operations more likely to work without defining a custom method.

One option to make these two types of iterations more compatible is to switch the (k,v) order around to (v,k) on Associative iteration. Then, if you leave off the key, you still get the value, and it would be like vector indexing.

HarlanH added some commits
@HarlanH HarlanH Merge branch 'master' of https://github.com/JuliaLang/julia 12d05b7
@HarlanH HarlanH work on adding NamedVector groups and more tests 80dec4d
@HarlanH HarlanH Merge branch 'refs/heads/testissues' 0d1d5ca
@HarlanH HarlanH NamedVector now inherits from AbstractVector
so "sum(x::NamedVector)" now works.
all tests pass.
needed to add a promote_rule(ByteString,ASCIIString) -- should that go elsewhere?
added similar(NamedVector), which is weird, but seems to work.
some duplicated code to deal with warnings -- is there a better way?
repl_show() just calls show().
0d412a8
@HarlanH
The Julia Language member

OK, could use some more feedback. See the comments on the most recent commit. Added groups for NamedVector, changed NamedVector to inherit from AbstractVector, and added more tests.

@HarlanH
The Julia Language member

Will make this a package instead. Closing pull request.

@HarlanH HarlanH closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 20, 2012
  1. @HarlanH

    NamedIndex and NamedVector types

    HarlanH committed
    initial commit, including simple test suite:
    
    ```julia
    load("named.jl")
    load("test.jl")
    tests("test/test_named.jl")
    ```
  2. @HarlanH
  3. @HarlanH
Commits on Aug 21, 2012
  1. @HarlanH

    reference in NamedIndex or NamedVector by symbol; assign for NamedVec…

    HarlanH committed
    …tor by symbol
    
    `nv[:cat] == nv["cat"]`
    
    Note that there's no `assign(NamedIndex)`, as it wouldn't make sense, but `nv[:cat] = 7` works.
  2. @HarlanH
Commits on Aug 26, 2012
  1. @HarlanH

    bug fix in extras/test.jl

    HarlanH committed
Commits on Aug 28, 2012
  1. @HarlanH
Commits on Aug 31, 2012
  1. @HarlanH
Commits on Sep 4, 2012
  1. @HarlanH
  2. @HarlanH

    groups are now implemented as Sets, separate from normal named elements

    HarlanH committed
    all tests pass
    replace_named!(ni, 1, "one") now works
  3. @HarlanH
Commits on Sep 8, 2012
  1. @HarlanH
  2. @HarlanH

    values(NamedVector) and find(NamedVector, value)

    HarlanH committed
    values() gives the underlying array values
    find() gives the position of the value in the array, or 0
Commits on Sep 21, 2012
  1. @HarlanH
  2. @HarlanH
  3. @HarlanH
  4. @HarlanH

    NamedVector now inherits from AbstractVector

    HarlanH committed
    so "sum(x::NamedVector)" now works.
    all tests pass.
    needed to add a promote_rule(ByteString,ASCIIString) -- should that go elsewhere?
    added similar(NamedVector), which is weird, but seems to work.
    some duplicated code to deal with warnings -- is there a better way?
    repl_show() just calls show().
Showing with 481 additions and 1 deletion.
  1. +365 −0 extras/named.jl
  2. +1 −1 extras/test.jl
  3. +115 −0 test/test_named.jl
View
365 extras/named.jl
@@ -0,0 +1,365 @@
+# When building data structures for use with interactive data manipuluation,
+# it is often handy to have data structures that can be efficiently indexed
+# by position (like a vector) as well as readably indexed by name (like a
+# hash table). The NamedIndex type here maps unique names to integer
+# indexes, looking up ByteString indexes in a Dict and passing other
+# index types through. The NamedVector type implements a simple use case,
+# with a backing vector of an arbitrary type.
+
+module Named
+
+import Base.*
+
+export NamedIndex, SimpleIndex, NamedVector,
+ length, isempty, names, copy, names!, replace_names!,
+ replace_names, has, keys, values, push, del, ref,
+ select, select_kv,
+ set_group, set_groups, get_group, get_groups, isgroup,
+ start, done, next, show
+
+# should this go elsewhere?
+promote_rule(::Type{Union(UTF8String,ASCIIString)}, ::Type{ASCIIString} ) = Union(UTF8String,ASCIIString)
+
+# an AbstractIndex is a thing that can be used to look up ordered things by name, but that
+# will also accept a position or set of positions or range or other things and pass them
+# through cleanly.
+# an NamedIndex is the usual implementation, with ByteString names.
+# a SimpleIndex only works if the things are integer indexes, which is weird.
+abstract AbstractIndex
+
+type NamedIndex <: AbstractIndex # an OrderedDict would be nice here...
+ lookup::Dict{ByteString,Indices} # name => names array position
+ groups::Dict # name => [other names]
+ names::Vector{ByteString}
+end
+
+function NamedIndex{T<:ByteString}(x::Vector{T})
+ NamedIndex(Dict{ByteString, Indices}(tuple(x...), tuple([1:length(x)]...)),
+ Dict(),
+ make_unique(convert(Vector{ByteString}, x)))
+end
+NamedIndex() = NamedIndex(Dict{ByteString,Indices}(), Dict(), ByteString[])
+
+length(x::NamedIndex) = length(x.names)
+names(x::NamedIndex) = copy(x.names)
+copy(x::NamedIndex) = NamedIndex(copy(x.lookup), copy(x.groups), copy(x.names))
+
+# note: replacing names removes any groups no longer present
+function names!(x::NamedIndex, nm::Vector)
+ if length(nm) != length(x)
+ error("lengths don't match.")
+ end
+ for i in 1:length(nm)
+ del(x.lookup, x.names[i])
+ x.lookup[nm[i]] = i
+ end
+ x.names = nm
+ clean_groups!(x, nm)
+ x.names
+end
+
+function clean_groups!(x::NamedIndex, nm::Vector)
+ for gr_key in keys(x.groups)
+ new_val = intersect(x.groups[gr_key], nm)
+ if length(new_val) == 0
+ del(x.groups, gr_key)
+ else
+ x.groups[gr_key] = new_val
+ end
+ end
+end
+
+function replace_names!(x::NamedIndex, indexes::Vector{Int}, new_names::Vector)
+ if length(indexes) != length(new_names)
+ error("lengths of indexes and new_names don't match.")
+ end
+ # it's tricky to figure out if we're ending up with dupes without trying it...
+ # for now, copy twice to avoid clobbering the caller if we end up failing
+ newx = copy(x)
+ for i = 1:length(indexes)
+ # remove the old lookup
+ del(newx.lookup, newx.names[i])
+ # add the new lookup
+ newx.lookup[new_names[i]] = indexes[i]
+ # replace the name
+ newx.names[indexes[i]] = new_names[i]
+ end
+ # make sure we didn't screw anything up
+ if length(keys(x.lookup)) != length(x.names)
+ error("replacement failed -- ended up with duplicate names")
+ end
+ x.names = newx.names
+ x.lookup = newx.lookup
+ clean_groups!(x, x.names)
+ x.names
+end
+function replace_names!(x::NamedIndex, from::Vector, to::Vector)
+ if length(from) != length(to)
+ error("lengths of from and to don't match.")
+ end
+ for idx in 1:length(from)
+ if has(x, from[idx]) && !has(x, to[idx])
+ x.lookup[to[idx]] = x.lookup[from[idx]]
+ x.names[x.lookup[from[idx]]] = to[idx]
+ del(x.lookup, from[idx])
+ end
+ end
+ clean_groups!(x, x.names)
+ x.names
+end
+replace_names!(x::NamedIndex, from, to) = replace_names!(x, [from], [to])
+replace_names(x::NamedIndex, from, to) = replace_names!(copy(x), from, to)
+
+# groups work with ref(), but not has(), keys(), etc
+has(x::NamedIndex, key) = has(x.lookup, key)
+keys(x::NamedIndex) = names(x)
+function push(x::NamedIndex, nm)
+ x.lookup[nm] = length(x) + 1
+ push(x.names, nm)
+end
+function del(x::NamedIndex, idx::Integer)
+ # reset the lookup's beyond the deleted item
+ for i in idx+1:length(x.names)
+ x.lookup[x.names[i]] = i - 1
+ end
+ del(x.lookup, x.names[idx])
+ del(x.names, idx)
+ # fix groups
+ clean_groups!(x, x.names)
+end
+function del(x::NamedIndex, nm)
+ if !has(x.lookup, nm)
+ return
+ end
+ idx = x.lookup[nm]
+ del(x, idx)
+end
+
+# ref should properly deal with the new groups
+function ref{T<:ByteString}(x::NamedIndex, idx::Vector{T})
+ # expand any groups, then do the comprehension
+ idx2 = T[]
+ for i in idx
+ if isgroup(x, i)
+ push(idx2, get_group(x, i))
+ else
+ push(idx2, i)
+ end
+ end
+ [[x.lookup[i] for i in idx]...]
+end
+function ref{T<:ByteString}(x::NamedIndex, idx::T)
+ if isgroup(x, idx)
+ ref(x, get_group(x, idx))
+ else
+ x.lookup[idx]
+ end
+end
+
+# if we get a symbol or a vector of symbols, convert to strings for ref
+ref(x::NamedIndex, idx::Symbol) = x[string(idx)]
+ref(x::NamedIndex, idx::Vector{Symbol}) = [[x.lookup[string(i)] for i in idx]...]
+
+# fall-throughs, when something other than the index type is passed
+ref(x::AbstractIndex, idx::Int) = idx
+ref(x::AbstractIndex, idx::Vector{Int}) = idx
+ref(x::AbstractIndex, idx::Range{Int}) = [idx]
+ref(x::AbstractIndex, idx::Range1{Int}) = [idx]
+ref(x::AbstractIndex, idx::Vector{Bool}) = [1:length(x)][idx]
+#ref(x::AbstractIndex, idx::AbstractDataVec{Bool}) = x[nareplace(idx, false)]
+#ref(x::AbstractIndex, idx::AbstractDataVec{Int}) = x[nafilter(idx)]
+
+type SimpleIndex <: AbstractIndex
+ length::Integer
+end
+SimpleIndex() = SimpleIndex(0)
+length(x::SimpleIndex) = x.length
+names(x::SimpleIndex) = nothing
+
+# Chris DuBois' idea: named
+function set_group(idx::NamedIndex, newgroup, names)
+ # confirm that the names exist
+ for name in names
+ if !has(idx, name)
+ error("can't add group referring to non-existent name!")
+ end
+ end
+ # add the group
+ idx.groups[newgroup] = Set(names...)
+end
+function set_groups(idx::NamedIndex, gr::Dict) # {ByteString,Vector{ByteString}}) breaks; duck type
+ for (k,v) in gr
+ set_group(idx, k, v)
+ end
+end
+get_group(idx::NamedIndex, name) = elements(idx.groups[name]) # set -> array
+get_groups(idx::NamedIndex) = idx.groups # returns a dict to sets, which may not be what you want
+isgroup(idx::NamedIndex, name::ByteString) = has(idx.groups, name)
+
+function show(io, idx::NamedIndex)
+ println(io, "$(length(idx))-element NamedIndex")
+ pretty_show(io, idx.groups)
+ for i = 1:min(length(idx), 9)
+ println(io, "$i = $(names(idx)[i])")
+ end
+ if length(idx) > 9
+ println(io, "...")
+ end
+end
+
+# special pretty-printer for groups, which are just Dicts.
+function pretty_show(io, gr)
+ allkeys = keys(gr)
+ for k = allkeys
+ print(io, "$(k): ")
+ print(io, join(gr[k], ", "))
+ if k == last(allkeys)
+ println(io)
+ else
+ print(io, "; ")
+ end
+ end
+end
+
+############ NamedVector #############
+
+type NamedVector{V} <: AbstractVector{V}
+ idx::NamedIndex
+ arr::Vector{V}
+ #NamedVector() = new(NamedIndex(), Array(V,0))
+end
+NamedVector() = NamedVector(NamedIndex(), Array(Any,0))
+NamedVector(T::Type) = NamedVector(NamedIndex(), Array(T,0))
+#NamedVector(i::NamedIndex, a::Vector) = NamedVector(i,a)
+
+function NamedVector{K<:ByteString,V}(a::Associative{K,V})
+ ret = NamedVector(V)
+ for k in keys(a)
+ ret[k] = a[k]
+ end
+ ret
+end
+
+function NamedVector{K<:ByteString,V}(keys::Vector{K}, values::Vector{V})
+ ret = NamedVector(V)
+ for i = 1:length(keys)
+ ret[keys[i]] = values[i]
+ end
+ ret
+end
+
+# similar's a little funny -- we make a similar array, but if the new vector's
+# longer than what we started with, the new keys are weird
+function similar(nv::NamedVector, element_type, dims)
+ new_arr = similar(nv.arr, element_type, dims)
+ if dims == size(nv)
+ new_idx = copy(nv.idx)
+ elseif dims < size(nv)
+ new_idx = NamedIndex(names(nv.idx)[1:dims[1]])
+ else
+ new_names = vcat(names(nv.idx), fill("X", dims[1]-length(nv)))
+ new_names = make_unique(new_names)
+ new_idx = NamedIndex(new_names)
+ end
+ NamedVector(new_idx, new_arr)
+end
+
+# assignment by a string replaces or appends
+function assign(id::NamedVector, v, key::ByteString)
+ if has(id.idx, key)
+ id.arr[id.idx[key]] = v
+ else
+ push(id.arr, v)
+ push(id.idx, key)
+ end
+end
+assign(id::NamedVector, v, key::Symbol) = assign(id, v, string(key))
+# assignment by an integer replaces or throws an error
+assign(id::NamedVector, v, pos::Integer) = id.arr[pos] = v
+assign(id::NamedVector, v, pos::Real) = assign(id, v, iround(pos))
+assign(id::NamedVector, v, pos) = id.arr[pos] = v
+
+ref{V,T<:Integer}(id::NamedVector{V}, ii::AbstractArray{T,1}) = id.arr[id.idx[ii]]
+ref(id::NamedVector, i::Integer) = id.arr[id.idx[i]]
+ref(id::NamedVector, i::Real) = ref(id, iround(i))
+ref(id::NamedVector, i) = id.arr[id.idx[i]]
+# ref gives the vector value; find gives the _position_ in the underlying NamedIndex
+find(id::NamedVector, v) = has(id, v) ? id.idx[v] : 0
+
+function get{K}(id::NamedVector{K}, key, deflt)
+ try
+ id[key]
+ catch e
+ deflt
+ end
+end
+
+size(nv::NamedVector) = size(nv.arr)
+names(nv::NamedVector) = names(nv.idx)
+keys(nv::NamedVector) = names(nv)
+values(nv::NamedVector) = nv.arr
+select(nv::NamedVector, r::Int64) = nv[r]
+select(nv::NamedVector, r) = nv[r]
+select_kv(nv::NamedVector, r) = (names(nv)[r], nv[r])
+
+has(id::NamedVector, key) = has(id.idx, key)
+isempty(id::NamedVector) = isempty(id.arr)
+length(id::NamedVector) = length(id.arr)
+
+start(id::NamedVector) = 1
+done(id::NamedVector, i) = i > length(id)
+next(id::NamedVector, i) = (id[i], i+1)
+
+function show(io, id::NamedVector)
+ n = names(id.idx)
+ println(io, "$(length(id))-element $(eltype(id.arr)) NamedVector")
+ pretty_show(io, id.idx.groups)
+ for i = 1:min(length(id), 9)
+ println(io, "$i, $(n[i]): $(id[i])")
+ end
+ if length(id) > 9
+ println(io, "...")
+ end
+end
+repl_show(io, id::NamedVector) = show(io, id::NamedVector)
+
+# copying has to copy everything, because otherwise you can append an item,
+# which can cause the vector to change length and break the original
+copy(nv::NamedVector) = NamedVector(copy(nv.idx), copy(nv.arr))
+
+function make_unique{S<:ByteString}(names::Vector{S})
+ x = NamedIndex()
+ names = copy(names)
+ dups = Int[]
+ for i in 1:length(names)
+ if has(x, names[i])
+ push(dups, i)
+ else
+ push(x, names[i])
+ end
+ end
+ for i in dups
+ nm = names[i]
+ newnm = nm
+ k = 1
+ while true
+ newnm = "$(nm)_$k"
+ if !has(x, newnm)
+ push(x, newnm)
+ break
+ end
+ k += 1
+ end
+ names[i] = newnm
+ end
+ names
+end
+
+set_group(v::NamedVector, newgroup, names) = set_group(v.idx, newgroup, names)
+set_groups(v::NamedVector, gr) = set_groups(v.idx, gr)
+
+get_group(v::NamedVector, name) = get_group(v.idx, name)
+get_groups(v::NamedVector) = get_groups(v.idx)
+isgroup(v::NamedVector, name::ByteString) = isgroup(v.idx, name)
+
+end #module
View
2 extras/test.jl
@@ -139,7 +139,7 @@ function _test(ex::Expr, expect_succeed::Bool)
tr.arg1 = eval(ex.args[1])
tr.arg2 = eval(ex.args[3])
elseif (ex.head == :call) # is it a helper we know about?
- if (ex.args[1] == :isapprox)
+ if (ex.args[1] == :approx_eq)
tr.operation = ex.args[1]
tr.arg1 = eval(ex.args[2])
tr.arg2 = eval(ex.args[3])
View
115 test/test_named.jl
@@ -0,0 +1,115 @@
+# use with extras/test.jl
+
+test_context("NamedIndex")
+
+require("named.jl")
+import Named.*
+
+test_group("creation")
+
+ni1 = NamedIndex(["one", "two", "three", "four", "five"])
+@test length(ni1) == 5
+
+test_group("access")
+@test ni1["one"] == 1
+@test ni1[2] == 2
+@test ni1[3:4] == [3,4]
+@test ni1[["one", "five"]] == [1,5]
+@testfails ni1["six"] == 6
+@test ni1[6] == 6 # falls through even if there's no name for this!
+@test ni1[[true, false, true, false, true]] == [1,3,5]
+@test ni1[:one] == ni1["one"]
+@test ni1[[:one, :five]] == [1, 5]
+
+test_group("changing names")
+ni2 = copy(ni1)
+@test ni1[1] == ni2[1]
+names!(ni2, ["uno", "dos", "tres", "quatro", "cinco"])
+@test ni2["uno"] == 1
+@test ni2[2] == 2
+
+replace_names!(ni2, ["tres", "quatro"], ["troix", "quatre"])
+@test ni2["troix"] == 3
+replace_names!(ni2, "cinco", "cinc")
+@test ni2["cinc"] == 5
+@test names(ni2) == ["uno", "dos", "troix", "quatre", "cinc"]
+replace_names!(ni2, 1, "ichi")
+replace_names!(ni2, [2,4], ["ni", "shi"])
+@test names(ni2) == ["ichi", "ni", "troix", "shi", "cinc"]
+@testfails replace_names!(ni2, 2, "ichi")
+
+test_group("methods")
+@test has(ni1, "one") == true
+@test keys(ni1) == ["one", "two", "three", "four", "five"]
+
+test_group("changing")
+ni3 = copy(ni1)
+push(ni3, "six")
+@test ni3["six"] == 6
+del(ni3, "one")
+del(ni3, 5)
+@test names(ni3) == ["two", "three", "four", "five"]
+
+test_group("groups")
+ni4 = copy(ni1)
+set_group(ni4, "odd", ["one", "three", "five"])
+@test all(ni4["odd"] .== [1, 3, 5])
+@test length(get_groups(ni4)) == 1
+set_groups(ni4, {"even"=>["two", "four"], "prime"=>["one", "two", "three", "five"]})
+@test length(ni4["prime"]) == 4
+@test isgroup(ni4, "nope") == false
+
+test_group("UTF-8")
+ni5 = copy(ni1)
+replace_names!(ni5, 4, "fourty€")
+@test ni5["fourty€"] == 4
+@test ni5[:fourty€] == 4
+
+test_context("NamedVector")
+
+test_group("basics")
+nams = ["one", "two", "three", "four"]
+vals = [11, 22, 33, 44]
+t1 = NamedVector()
+for i = 1:4
+ t1[nams[i]] = vals[i]
+end
+@test t1["one"] == 11
+@test t1[2] == 22
+@test t1[1:3] == [11, 22, 33]
+@test t1[["four", "two"]] == [44, 22]
+@test t1[:one] == 11
+@test t1[[:four, :two]] == [44, 22]
+@test first(t1) == 11
+@test last(t1) == 44
+@test sum(t1) == 110
+@test sprint(show, t1) == "4-element Any NamedVector\n1, one: 11\n2, two: 22\n3, three: 33\n4, four: 44\n"
+
+test_group("iteration")
+@test all([x::Int64 for x in t1] .== [11, 22, 33, 44])
+@test select(t1, 3) == 33
+@test select_kv(t1, 3) == ("three", 33)
+
+test_group("construction")
+xx=NamedVector({"asdf"=>1, "qwerty"=>2})
+@test typeof(xx) == NamedVector{Int64}
+xx2=NamedVector(["asdf", "querty", "uiop"], [11,22,33])
+@test typeof(xx2) == NamedVector{Int64}
+@test xx2["querty"] == 22
+@test xx2[3] == 33
+
+test_group("copying")
+t2 = copy(t1)
+# neither adding nor modifying should affect t1
+t2["five"] = 55
+t2["one"] = 111
+@test t2["five"] == 55
+@test t2["one"] == 111
+@testfails t1["five"]
+@test t1["one"] == 11
+
+test_group("groups")
+t3 = copy(t1)
+set_group(t3, "odd", ["one", "three"])
+@test all(t3["odd"] .== [11, 33])
+@test length(get_groups(t3)) == 1
Something went wrong with that request. Please try again.