Files
Nim/lib/pure/collections/sets.nim
c-blake a0b33f9408 Add hashWangYi1 (#13823)
* Unwind just the "pseudorandom probing" (whole hash-code-keyed variable
stride double hashing) part of recent sets & tables changes (which has
still been causing bugs over a month later (e.g., two days ago
https://github.com/nim-lang/Nim/issues/13794) as well as still having
several "figure this out" implementation question comments in them (see
just diffs of this PR).

This topic has been discussed in many places:
  https://github.com/nim-lang/Nim/issues/13393
  https://github.com/nim-lang/Nim/pull/13418
  https://github.com/nim-lang/Nim/pull/13440
  https://github.com/nim-lang/Nim/issues/13794

Alternative/non-mandatory stronger integer hashes (or vice-versa opt-in
identity hashes) are a better solution that is more general (no illusion
of one hard-coded sequence solving all problems) while retaining the
virtues of linear probing such as cache obliviousness and age-less tables
under delete-heavy workloads (still untested after a month of this change).

The only real solution for truly adversarial keys is a hash keyed off of
data unobservable to attackers.  That all fits better with a few families
of user-pluggable/define-switchable hashes which can be provided in a
separate PR more about `hashes.nim`.

This PR carefully preserves the better (but still hard coded!) probing
of the  `intsets` and other recent fixes like `move` annotations, hash
order invariant tests, `intsets.missingOrExcl` fixing, and the move of
`rightSize` into `hashcommon.nim`.

* Fix `data.len` -> `dataLen` problem.

* This is an alternate resolution to https://github.com/nim-lang/Nim/issues/13393
(which arguably could be resolved outside the stdlib).

Add version1 of Wang Yi's hash specialized to 8 byte integers.  This gives
simple help to users having trouble with overly colliding hash(key)s.  I.e.,
  A) `import hashes; proc hash(x: myInt): Hash = hashWangYi1(int(x))`
      in the instantiation context of a `HashSet` or `Table`
or
  B) more globally, compile with `nim c -d:hashWangYi1`.

No hash can be all things to all use cases, but this one is A) vetted to
scramble well by the SMHasher test suite (a necessarily limited but far
more thorough test than prior proposals here), B) only a few ALU ops on
many common CPUs, and C) possesses an easy via "grade school multi-digit
multiplication" fall back for weaker deployment contexts.

Some people might want to stampede ahead unbridled, but my view is that a
good plan is to
  A) include this in the stdlib for a release or three to let people try it
     on various key sets nim-core could realistically never access/test
     (maybe mentioning it in the changelog so people actually try it out),
  B) have them report problems (if any),
  C) if all seems good, make the stdlib more novice friendly by adding
     `hashIdentity(x)=x` and changing the default `hash() = hashWangYi1`
     with some `when defined` rearranging so users can `-d:hashIdentity`
     if they want the old behavior back.
This plan is compatible with any number of competing integer hashes if
people want to add them.  I would strongly recommend they all *at least*
pass the SMHasher suite since the idea here is to become more friendly to
novices who do not generally understand hashing failure modes.

* Re-organize to work around `when nimvm` limitations; Add some tests; Add
a changelog.md entry.

* Add less than 64-bit CPU when fork.

* Fix decl instead of call typo.

* First attempt at fixing range error on 32-bit platforms; Still do the
arithmetic in doubled up 64-bit, but truncate the hash to the lower
32-bits, but then still return `uint64` to be the same.  So, type
correct but truncated hash value.  Update `thashes.nim` as well.

* A second try at making 32-bit mode CI work.

* Use a more systematic identifier convention than Wang Yi's code.

* Fix test that was wrong for as long as `toHashSet` used `rightSize` (a
very long time, I think).  `$a`/`$b` depend on iteration order which
varies with table range reduced hash order which varies with range for
some `hash()`.  With 3 elements, 3!=6 is small and we've just gotten
lucky with past experimental `hash()` changes.  An alternate fix here
would be to not stringify but use the HashSet operators, but it is not
clear that doesn't alter the "spirit" of the test.

* Fix another stringified test depending upon hash order.

* Oops - revert the string-keyed test.

* Fix another stringify test depending on hash order.

* Add a better than always zero `defined(js)` branch.

* It turns out to be easy to just work all in `BigInt` inside JS and thus
guarantee the same low order bits of output hashes (for `isSafeInteger`
input numbers).  Since `hashWangYi1` output bits are equally random in
all their bits, this means that tables will be safely scrambled for table
sizes up to 2**32 or 4 gigaentries which is probably fine, as long as the
integer keys are all < 2**53 (also likely fine).  (I'm unsure why the
infidelity with C/C++ back ends cut off is 32, not 53 bits.)

Since HashSet & Table only use the low order bits, a quick corollary of
this is that `$` on most int-keyed sets/tables will be the same in all
the various back ends which seems a nice-to-have trait.

* These string hash tests fail for me locally.  Maybe this is what causes
the CI hang for testament pcat collections?

* Oops. That failure was from me manually patching string hash in hashes.  Revert.

* Import more test improvements from https://github.com/nim-lang/Nim/pull/13410

* Fix bug where I swapped order when reverting the test.  Ack.

* Oh, just accept either order like more and more hash tests.

* Iterate in the same order.

* `return` inside `emit` made us skip `popFrame` causing weird troubles.

* Oops - do Windows branch also.

* `nimV1hash` -> multiply-mnemonic, type-scoped `nimIntHash1` (mnemonic
resolutions are "1 == identity", 1 for Nim Version 1, 1 for
first/simplest/fastest in a series of possibilities.  Should be very
easy to remember.)

* Re-organize `when nimvm` logic to be a strict `when`-`else`.

* Merge other changes.

* Lift constants to a common area.

* Fall back to identity hash when `BigInt` is unavailable.

* Increase timeout slightly (probably just real-time perturbation of CI
system performance).
2020-04-15 20:11:18 +02:00

1238 lines
36 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
#
# Nim's Runtime Library
# (c) Copyright 2012 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## The ``sets`` module implements an efficient `hash set`:idx: and
## ordered hash set.
##
## Hash sets are different from the `built in set type
## <manual.html#types-set-type>`_. Sets allow you to store any value that can be
## `hashed <hashes.html>`_ and they don't contain duplicate entries.
##
## Common usages of sets:
## * removing duplicates from a container by converting it with `toHashSet proc
## <#toHashSet,openArray[A]>`_ (see also `sequtils.deduplicate proc
## <sequtils.html#deduplicate,openArray[T],bool>`_)
## * membership testing
## * mathematical operations on two sets, such as
## `union <#union,HashSet[A],HashSet[A]>`_,
## `intersection <#intersection,HashSet[A],HashSet[A]>`_,
## `difference <#difference,HashSet[A],HashSet[A]>`_, and
## `symmetric difference <#symmetricDifference,HashSet[A],HashSet[A]>`_
##
## .. code-block::
## echo toHashSet([9, 5, 1]) # {9, 1, 5}
## echo toOrderedSet([9, 5, 1]) # {9, 5, 1}
##
## let
## s1 = toHashSet([9, 5, 1])
## s2 = toHashSet([3, 5, 7])
##
## echo s1 + s2 # {9, 1, 3, 5, 7}
## echo s1 - s2 # {1, 9}
## echo s1 * s2 # {5}
## echo s1 -+- s2 # {9, 1, 3, 7}
##
##
## Note: The data types declared here have *value semantics*: This means
## that ``=`` performs a copy of the set.
##
## **See also:**
## * `intsets module <intsets.html>`_ for efficient int sets
## * `tables module <tables.html>`_ for hash tables
import
hashes, math
{.pragma: myShallow.}
when not defined(nimhygiene):
{.pragma: dirty.}
# For "integer-like A" that are too big for intsets/bit-vectors to be practical,
# it would be best to shrink hcode to the same size as the integer. Larger
# codes should never be needed, and this can pack more entries per cache-line.
# Losing hcode entirely is also possible - if some element value is forbidden.
type
KeyValuePair[A] = tuple[hcode: Hash, key: A]
KeyValuePairSeq[A] = seq[KeyValuePair[A]]
HashSet*[A] {.myShallow.} = object ## \
## A generic hash set.
##
## Use `init proc <#init,HashSet[A],int>`_ or `initHashSet proc <#initHashSet,int>`_
## before calling other procs on it.
data: KeyValuePairSeq[A]
counter: int
type
OrderedKeyValuePair[A] = tuple[
hcode: Hash, next: int, key: A]
OrderedKeyValuePairSeq[A] = seq[OrderedKeyValuePair[A]]
OrderedSet*[A] {.myShallow.} = object ## \
## A generic hash set that remembers insertion order.
##
## Use `init proc <#init,OrderedSet[A],int>`_ or `initOrderedSet proc
## <#initOrderedSet,int>`_ before calling other procs on it.
data: OrderedKeyValuePairSeq[A]
counter, first, last: int
const
defaultInitialSize* = 64
include setimpl
# ---------------------------------------------------------------------
# ------------------------------ HashSet ------------------------------
# ---------------------------------------------------------------------
proc init*[A](s: var HashSet[A], initialSize = defaultInitialSize) =
## Initializes a hash set.
##
## The `initialSize` parameter needs to be a power of two (default: 64).
## If you need to accept runtime values for this, you can use
## `math.nextPowerOfTwo proc <math.html#nextPowerOfTwo,int>`_ or
## `rightSize proc <#rightSize,Natural>`_ from this module.
##
## Starting from Nim v0.20, sets are initialized by default and it is
## not necessary to call this function explicitly.
##
## You can call this proc on a previously initialized hash set, which will
## discard all its values. This might be more convenient than iterating over
## existing values and calling `excl() <#excl,HashSet[A],A>`_ on them.
##
## See also:
## * `initHashSet proc <#initHashSet,int>`_
## * `toHashSet proc <#toHashSet,openArray[A]>`_
runnableExamples:
var a: HashSet[int]
assert(not a.isValid)
init(a)
assert a.isValid
initImpl(s, initialSize)
proc initHashSet*[A](initialSize = defaultInitialSize): HashSet[A] =
## Wrapper around `init proc <#init,HashSet[A],int>`_ for initialization of
## hash sets.
##
## Returns an empty hash set you can assign directly in ``var`` blocks in a
## single line.
##
## Starting from Nim v0.20, sets are initialized by default and it is
## not necessary to call this function explicitly.
##
## See also:
## * `toHashSet proc <#toHashSet,openArray[A]>`_
runnableExamples:
var a = initHashSet[int]()
a.incl(3)
assert len(a) == 1
result.init(initialSize)
proc `[]`*[A](s: var HashSet[A], key: A): var A =
## Returns the element that is actually stored in `s` which has the same
## value as `key` or raises the ``KeyError`` exception.
##
## This is useful when one overloaded `hash` and `==` but still needs
## reference semantics for sharing.
var hc: Hash
var index = rawGet(s, key, hc)
if index >= 0: result = s.data[index].key
else:
when compiles($key):
raise newException(KeyError, "key not found: " & $key)
else:
raise newException(KeyError, "key not found")
proc contains*[A](s: HashSet[A], key: A): bool =
## Returns true if `key` is in `s`.
##
## This allows the usage of `in` operator.
##
## See also:
## * `incl proc <#incl,HashSet[A],A>`_
## * `containsOrIncl proc <#containsOrIncl,HashSet[A],A>`_
runnableExamples:
var values = initHashSet[int]()
assert(not values.contains(2))
assert 2 notin values
values.incl(2)
assert values.contains(2)
assert 2 in values
var hc: Hash
var index = rawGet(s, key, hc)
result = index >= 0
proc incl*[A](s: var HashSet[A], key: A) =
## Includes an element `key` in `s`.
##
## This doesn't do anything if `key` is already in `s`.
##
## See also:
## * `excl proc <#excl,HashSet[A],A>`_ for excluding an element
## * `incl proc <#incl,HashSet[A],HashSet[A]>`_ for including other set
## * `containsOrIncl proc <#containsOrIncl,HashSet[A],A>`_
runnableExamples:
var values = initHashSet[int]()
values.incl(2)
values.incl(2)
assert values.len == 1
inclImpl()
proc incl*[A](s: var HashSet[A], other: HashSet[A]) =
## Includes all elements from `other` set into `s` (must be declared as `var`).
##
## This is the in-place version of `s + other <#+,HashSet[A],HashSet[A]>`_.
##
## See also:
## * `excl proc <#excl,HashSet[A],HashSet[A]>`_ for excluding other set
## * `incl proc <#incl,HashSet[A],A>`_ for including an element
## * `containsOrIncl proc <#containsOrIncl,HashSet[A],A>`_
runnableExamples:
var
values = toHashSet([1, 2, 3])
others = toHashSet([3, 4, 5])
values.incl(others)
assert values.len == 5
for item in other: incl(s, item)
proc toHashSet*[A](keys: openArray[A]): HashSet[A] =
## Creates a new hash set that contains the members of the given
## collection (seq, array, or string) `keys`.
##
## Duplicates are removed.
##
## See also:
## * `initHashSet proc <#initHashSet,int>`_
runnableExamples:
let
a = toHashSet([5, 3, 2])
b = toHashSet("abracadabra")
assert len(a) == 3
## a == {2, 3, 5}
assert len(b) == 5
## b == {'a', 'b', 'c', 'd', 'r'}
result = initHashSet[A](rightSize(keys.len))
for key in items(keys): result.incl(key)
iterator items*[A](s: HashSet[A]): A =
## Iterates over elements of the set `s`.
##
## If you need a sequence with the elements you can use `sequtils.toSeq
## template <sequtils.html#toSeq.t,untyped>`_.
##
## .. code-block::
## type
## pair = tuple[a, b: int]
## var
## a, b = initHashSet[pair]()
## a.incl((2, 3))
## a.incl((3, 2))
## a.incl((2, 3))
## for x, y in a.items:
## b.incl((x - 2, y + 1))
## assert a.len == 2
## echo b
## # --> {(a: 1, b: 3), (a: 0, b: 4)}
for h in 0 .. high(s.data):
if isFilled(s.data[h].hcode): yield s.data[h].key
proc containsOrIncl*[A](s: var HashSet[A], key: A): bool =
## Includes `key` in the set `s` and tells if `key` was already in `s`.
##
## The difference with regards to the `incl proc <#incl,HashSet[A],A>`_ is
## that this proc returns `true` if `s` already contained `key`. The
## proc will return `false` if `key` was added as a new value to `s` during
## this call.
##
## See also:
## * `incl proc <#incl,HashSet[A],A>`_ for including an element
## * `incl proc <#incl,HashSet[A],HashSet[A]>`_ for including other set
## * `missingOrExcl proc <#missingOrExcl,HashSet[A],A>`_
runnableExamples:
var values = initHashSet[int]()
assert values.containsOrIncl(2) == false
assert values.containsOrIncl(2) == true
assert values.containsOrIncl(3) == false
containsOrInclImpl()
proc excl*[A](s: var HashSet[A], key: A) =
## Excludes `key` from the set `s`.
##
## This doesn't do anything if `key` is not found in `s`.
##
## See also:
## * `incl proc <#incl,HashSet[A],A>`_ for including an element
## * `excl proc <#excl,HashSet[A],HashSet[A]>`_ for excluding other set
## * `missingOrExcl proc <#missingOrExcl,HashSet[A],A>`_
runnableExamples:
var s = toHashSet([2, 3, 6, 7])
s.excl(2)
s.excl(2)
assert s.len == 3
discard exclImpl(s, key)
proc excl*[A](s: var HashSet[A], other: HashSet[A]) =
## Excludes all elements of `other` set from `s`.
##
## This is the in-place version of `s - other <#-,HashSet[A],HashSet[A]>`_.
##
## See also:
## * `incl proc <#incl,HashSet[A],HashSet[A]>`_ for including other set
## * `excl proc <#excl,HashSet[A],A>`_ for excluding an element
## * `missingOrExcl proc <#missingOrExcl,HashSet[A],A>`_
runnableExamples:
var
numbers = toHashSet([1, 2, 3, 4, 5])
even = toHashSet([2, 4, 6, 8])
numbers.excl(even)
assert len(numbers) == 3
## numbers == {1, 3, 5}
for item in other: discard exclImpl(s, item)
proc missingOrExcl*[A](s: var HashSet[A], key: A): bool =
## Excludes `key` in the set `s` and tells if `key` was already missing from `s`.
##
## The difference with regards to the `excl proc <#excl,HashSet[A],A>`_ is
## that this proc returns `true` if `key` was missing from `s`.
## The proc will return `false` if `key` was in `s` and it was removed
## during this call.
##
## See also:
## * `excl proc <#excl,HashSet[A],A>`_ for excluding an element
## * `excl proc <#excl,HashSet[A],HashSet[A]>`_ for excluding other set
## * `containsOrIncl proc <#containsOrIncl,HashSet[A],A>`_
runnableExamples:
var s = toHashSet([2, 3, 6, 7])
assert s.missingOrExcl(4) == true
assert s.missingOrExcl(6) == false
assert s.missingOrExcl(6) == true
exclImpl(s, key)
proc pop*[A](s: var HashSet[A]): A =
## Remove and return an arbitrary element from the set `s`.
##
## Raises KeyError if the set `s` is empty.
##
## See also:
## * `clear proc <#clear,HashSet[A]>`_
runnableExamples:
var s = toHashSet([2, 1])
assert [s.pop, s.pop] in [[1, 2], [2,1]] # order unspecified
doAssertRaises(KeyError, echo s.pop)
for h in 0 .. high(s.data):
if isFilled(s.data[h].hcode):
result = s.data[h].key
excl(s, result)
return result
raise newException(KeyError, "set is empty")
proc clear*[A](s: var HashSet[A]) =
## Clears the HashSet back to an empty state, without shrinking
## any of the existing storage.
##
## `O(n)` operation, where `n` is the size of the hash bucket.
##
## See also:
## * `pop proc <#pop,HashSet[A]>`_
runnableExamples:
var s = toHashSet([3, 5, 7])
clear(s)
assert len(s) == 0
s.counter = 0
for i in 0 ..< s.data.len:
s.data[i].hcode = 0
s.data[i].key = default(type(s.data[i].key))
proc len*[A](s: HashSet[A]): int =
## Returns the number of elements in `s`.
##
## Due to an implementation detail you can call this proc on variables which
## have not been initialized yet. The proc will return zero as the length
## then.
runnableExamples:
var a: HashSet[string]
assert len(a) == 0
let s = toHashSet([3, 5, 7])
assert len(s) == 3
result = s.counter
proc card*[A](s: HashSet[A]): int =
## Alias for `len() <#len,HashSet[A]>`_.
##
## Card stands for the `cardinality
## <http://en.wikipedia.org/wiki/Cardinality>`_ of a set.
result = s.counter
proc union*[A](s1, s2: HashSet[A]): HashSet[A] =
## Returns the union of the sets `s1` and `s2`.
##
## The same as `s1 + s2 <#+,HashSet[A],HashSet[A]>`_.
##
## The union of two sets is represented mathematically as *A B* and is the
## set of all objects that are members of `s1`, `s2` or both.
##
## See also:
## * `intersection proc <#intersection,HashSet[A],HashSet[A]>`_
## * `difference proc <#difference,HashSet[A],HashSet[A]>`_
## * `symmetricDifference proc <#symmetricDifference,HashSet[A],HashSet[A]>`_
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = union(a, b)
assert c == toHashSet(["a", "b", "c"])
result = s1
incl(result, s2)
proc intersection*[A](s1, s2: HashSet[A]): HashSet[A] =
## Returns the intersection of the sets `s1` and `s2`.
##
## The same as `s1 * s2 <#*,HashSet[A],HashSet[A]>`_.
##
## The intersection of two sets is represented mathematically as *A ∩ B* and
## is the set of all objects that are members of `s1` and `s2` at the same
## time.
##
## See also:
## * `union proc <#union,HashSet[A],HashSet[A]>`_
## * `difference proc <#difference,HashSet[A],HashSet[A]>`_
## * `symmetricDifference proc <#symmetricDifference,HashSet[A],HashSet[A]>`_
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = intersection(a, b)
assert c == toHashSet(["b"])
result = initHashSet[A](max(min(s1.data.len, s2.data.len), 2))
for item in s1:
if item in s2: incl(result, item)
proc difference*[A](s1, s2: HashSet[A]): HashSet[A] =
## Returns the difference of the sets `s1` and `s2`.
##
## The same as `s1 - s2 <#-,HashSet[A],HashSet[A]>`_.
##
## The difference of two sets is represented mathematically as *A B* and is
## the set of all objects that are members of `s1` and not members of `s2`.
##
## See also:
## * `union proc <#union,HashSet[A],HashSet[A]>`_
## * `intersection proc <#intersection,HashSet[A],HashSet[A]>`_
## * `symmetricDifference proc <#symmetricDifference,HashSet[A],HashSet[A]>`_
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = difference(a, b)
assert c == toHashSet(["a"])
result = initHashSet[A]()
for item in s1:
if not contains(s2, item):
incl(result, item)
proc symmetricDifference*[A](s1, s2: HashSet[A]): HashSet[A] =
## Returns the symmetric difference of the sets `s1` and `s2`.
##
## The same as `s1 -+- s2 <#-+-,HashSet[A],HashSet[A]>`_.
##
## The symmetric difference of two sets is represented mathematically as *A △
## B* or *A ⊖ B* and is the set of all objects that are members of `s1` or
## `s2` but not both at the same time.
##
## See also:
## * `union proc <#union,HashSet[A],HashSet[A]>`_
## * `intersection proc <#intersection,HashSet[A],HashSet[A]>`_
## * `difference proc <#difference,HashSet[A],HashSet[A]>`_
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = symmetricDifference(a, b)
assert c == toHashSet(["a", "c"])
result = s1
for item in s2:
if containsOrIncl(result, item): excl(result, item)
proc `+`*[A](s1, s2: HashSet[A]): HashSet[A] {.inline.} =
## Alias for `union(s1, s2) <#union,HashSet[A],HashSet[A]>`_.
result = union(s1, s2)
proc `*`*[A](s1, s2: HashSet[A]): HashSet[A] {.inline.} =
## Alias for `intersection(s1, s2) <#intersection,HashSet[A],HashSet[A]>`_.
result = intersection(s1, s2)
proc `-`*[A](s1, s2: HashSet[A]): HashSet[A] {.inline.} =
## Alias for `difference(s1, s2) <#difference,HashSet[A],HashSet[A]>`_.
result = difference(s1, s2)
proc `-+-`*[A](s1, s2: HashSet[A]): HashSet[A] {.inline.} =
## Alias for `symmetricDifference(s1, s2)
## <#symmetricDifference,HashSet[A],HashSet[A]>`_.
result = symmetricDifference(s1, s2)
proc disjoint*[A](s1, s2: HashSet[A]): bool =
## Returns `true` if the sets `s1` and `s2` have no items in common.
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
assert disjoint(a, b) == false
assert disjoint(a, b - a) == true
for item in s1:
if item in s2: return false
return true
proc `<`*[A](s, t: HashSet[A]): bool =
## Returns true if `s` is a strict or proper subset of `t`.
##
## A strict or proper subset `s` has all of its members in `t` but `t` has
## more elements than `s`.
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = intersection(a, b)
assert c < a and c < b
assert(not (a < a))
s.counter != t.counter and s <= t
proc `<=`*[A](s, t: HashSet[A]): bool =
## Returns true if `s` is a subset of `t`.
##
## A subset `s` has all of its members in `t` and `t` doesn't necessarily
## have more members than `s`. That is, `s` can be equal to `t`.
runnableExamples:
let
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = intersection(a, b)
assert c <= a and c <= b
assert a <= a
result = false
if s.counter > t.counter: return
result = true
for item in items(s):
if not(t.contains(item)):
result = false
return
proc `==`*[A](s, t: HashSet[A]): bool =
## Returns true if both `s` and `t` have the same members and set size.
runnableExamples:
var
a = toHashSet([1, 2])
b = toHashSet([2, 1])
assert a == b
s.counter == t.counter and s <= t
proc map*[A, B](data: HashSet[A], op: proc (x: A): B {.closure.}): HashSet[B] =
## Returns a new set after applying `op` proc on each of the elements of
##`data` set.
##
## You can use this proc to transform the elements from a set.
runnableExamples:
let
a = toHashSet([1, 2, 3])
b = a.map(proc (x: int): string = $x)
assert b == toHashSet(["1", "2", "3"])
result = initHashSet[B]()
for item in items(data): result.incl(op(item))
proc hash*[A](s: HashSet[A]): Hash =
## Hashing of HashSet.
for h in 0 .. high(s.data):
result = result xor s.data[h].hcode
result = !$result
proc `$`*[A](s: HashSet[A]): string =
## Converts the set `s` to a string, mostly for logging and printing purposes.
##
## Don't use this proc for serialization, the representation may change at
## any moment and values are not escaped.
##
## **Examples:**
##
## .. code-block::
## echo toHashSet([2, 4, 5])
## # --> {2, 4, 5}
## echo toHashSet(["no", "esc'aping", "is \" provided"])
## # --> {no, esc'aping, is " provided}
dollarImpl()
proc initSet*[A](initialSize = defaultInitialSize): HashSet[A] {.deprecated:
"Deprecated since v0.20, use 'initHashSet'".} = initHashSet[A](initialSize)
proc toSet*[A](keys: openArray[A]): HashSet[A] {.deprecated:
"Deprecated since v0.20, use 'toHashSet'".} = toHashSet[A](keys)
proc isValid*[A](s: HashSet[A]): bool {.deprecated:
"Deprecated since v0.20; sets are initialized by default".} =
## Returns `true` if the set has been initialized (with `initHashSet proc
## <#initHashSet,int>`_ or `init proc <#init,HashSet[A],int>`_).
##
## **Examples:**
##
## .. code-block ::
## proc savePreferences(options: HashSet[string]) =
## assert options.isValid, "Pass an initialized set!"
## # Do stuff here, may crash in release builds!
result = s.data.len > 0
# ---------------------------------------------------------------------
# --------------------------- OrderedSet ------------------------------
# ---------------------------------------------------------------------
template forAllOrderedPairs(yieldStmt: untyped) {.dirty.} =
if s.data.len > 0:
var h = s.first
var idx = 0
while h >= 0:
var nxt = s.data[h].next
if isFilled(s.data[h].hcode):
yieldStmt
inc(idx)
h = nxt
proc init*[A](s: var OrderedSet[A], initialSize = defaultInitialSize) =
## Initializes an ordered hash set.
##
## The `initialSize` parameter needs to be a power of two (default: 64).
## If you need to accept runtime values for this, you can use
## `math.nextPowerOfTwo proc <math.html#nextPowerOfTwo,int>`_ or
## `rightSize proc <#rightSize,Natural>`_ from this module.
##
## Starting from Nim v0.20, sets are initialized by default and it is
## not necessary to call this function explicitly.
##
## You can call this proc on a previously initialized hash set, which will
## discard all its values. This might be more convenient than iterating over
## existing values and calling `excl() <#excl,HashSet[A],A>`_ on them.
##
## See also:
## * `initOrderedSet proc <#initOrderedSet,int>`_
## * `toOrderedSet proc <#toOrderedSet,openArray[A]>`_
runnableExamples:
var a: OrderedSet[int]
assert(not a.isValid)
init(a)
assert a.isValid
initImpl(s, initialSize)
proc initOrderedSet*[A](initialSize = defaultInitialSize): OrderedSet[A] =
## Wrapper around `init proc <#init,OrderedSet[A],int>`_ for initialization of
## ordered hash sets.
##
## Returns an empty ordered hash set you can assign directly in ``var`` blocks
## in a single line.
##
## Starting from Nim v0.20, sets are initialized by default and it is
## not necessary to call this function explicitly.
##
## See also:
## * `toOrderedSet proc <#toOrderedSet,openArray[A]>`_
runnableExamples:
var a = initOrderedSet[int]()
a.incl(3)
assert len(a) == 1
result.init(initialSize)
proc toOrderedSet*[A](keys: openArray[A]): OrderedSet[A] =
## Creates a new hash set that contains the members of the given
## collection (seq, array, or string) `keys`.
##
## Duplicates are removed.
##
## See also:
## * `initOrderedSet proc <#initOrderedSet,int>`_
runnableExamples:
let
a = toOrderedSet([5, 3, 2])
b = toOrderedSet("abracadabra")
assert len(a) == 3
## a == {5, 3, 2} # different than in HashSet
assert len(b) == 5
## b == {'a', 'b', 'r', 'c', 'd'} # different than in HashSet
result = initOrderedSet[A](rightSize(keys.len))
for key in items(keys): result.incl(key)
proc contains*[A](s: OrderedSet[A], key: A): bool =
## Returns true if `key` is in `s`.
##
## This allows the usage of `in` operator.
##
## See also:
## * `incl proc <#incl,OrderedSet[A],A>`_
## * `containsOrIncl proc <#containsOrIncl,OrderedSet[A],A>`_
runnableExamples:
var values = initOrderedSet[int]()
assert(not values.contains(2))
assert 2 notin values
values.incl(2)
assert values.contains(2)
assert 2 in values
var hc: Hash
var index = rawGet(s, key, hc)
result = index >= 0
proc incl*[A](s: var OrderedSet[A], key: A) =
## Includes an element `key` in `s`.
##
## This doesn't do anything if `key` is already in `s`.
##
## See also:
## * `excl proc <#excl,OrderedSet[A],A>`_ for excluding an element
## * `incl proc <#incl,HashSet[A],OrderedSet[A]>`_ for including other set
## * `containsOrIncl proc <#containsOrIncl,OrderedSet[A],A>`_
runnableExamples:
var values = initOrderedSet[int]()
values.incl(2)
values.incl(2)
assert values.len == 1
inclImpl()
proc incl*[A](s: var HashSet[A], other: OrderedSet[A]) =
## Includes all elements from the OrderedSet `other` into
## HashSet `s` (must be declared as `var`).
##
## See also:
## * `incl proc <#incl,OrderedSet[A],A>`_ for including an element
## * `containsOrIncl proc <#containsOrIncl,OrderedSet[A],A>`_
runnableExamples:
var
values = toHashSet([1, 2, 3])
others = toOrderedSet([3, 4, 5])
values.incl(others)
assert values.len == 5
for item in items(other): incl(s, item)
proc containsOrIncl*[A](s: var OrderedSet[A], key: A): bool =
## Includes `key` in the set `s` and tells if `key` was already in `s`.
##
## The difference with regards to the `incl proc <#incl,OrderedSet[A],A>`_ is
## that this proc returns `true` if `s` already contained `key`. The
## proc will return false if `key` was added as a new value to `s` during
## this call.
##
## See also:
## * `incl proc <#incl,OrderedSet[A],A>`_ for including an element
## * `missingOrExcl proc <#missingOrExcl,OrderedSet[A],A>`_
runnableExamples:
var values = initOrderedSet[int]()
assert values.containsOrIncl(2) == false
assert values.containsOrIncl(2) == true
assert values.containsOrIncl(3) == false
containsOrInclImpl()
proc excl*[A](s: var OrderedSet[A], key: A) =
## Excludes `key` from the set `s`. Efficiency: `O(n)`.
##
## This doesn't do anything if `key` is not found in `s`.
##
## See also:
## * `incl proc <#incl,OrderedSet[A],A>`_ for including an element
## * `missingOrExcl proc <#missingOrExcl,OrderedSet[A],A>`_
runnableExamples:
var s = toOrderedSet([2, 3, 6, 7])
s.excl(2)
s.excl(2)
assert s.len == 3
discard exclImpl(s, key)
proc missingOrExcl*[A](s: var OrderedSet[A], key: A): bool =
## Excludes `key` in the set `s` and tells if `key` was already missing from `s`.
## Efficiency: O(n).
##
## The difference with regards to the `excl proc <#excl,OrderedSet[A],A>`_ is
## that this proc returns `true` if `key` was missing from `s`.
## The proc will return `false` if `key` was in `s` and it was removed
## during this call.
##
## See also:
## * `excl proc <#excl,OrderedSet[A],A>`_
## * `containsOrIncl proc <#containsOrIncl,OrderedSet[A],A>`_
runnableExamples:
var s = toOrderedSet([2, 3, 6, 7])
assert s.missingOrExcl(4) == true
assert s.missingOrExcl(6) == false
assert s.missingOrExcl(6) == true
exclImpl(s, key)
proc clear*[A](s: var OrderedSet[A]) =
## Clears the OrderedSet back to an empty state, without shrinking
## any of the existing storage.
##
## `O(n)` operation where `n` is the size of the hash bucket.
runnableExamples:
var s = toOrderedSet([3, 5, 7])
clear(s)
assert len(s) == 0
s.counter = 0
s.first = -1
s.last = -1
for i in 0 ..< s.data.len:
s.data[i].hcode = 0
s.data[i].next = 0
s.data[i].key = default(type(s.data[i].key))
proc len*[A](s: OrderedSet[A]): int {.inline.} =
## Returns the number of elements in `s`.
##
## Due to an implementation detail you can call this proc on variables which
## have not been initialized yet. The proc will return zero as the length
## then.
runnableExamples:
var a: OrderedSet[string]
assert len(a) == 0
let s = toHashSet([3, 5, 7])
assert len(s) == 3
result = s.counter
proc card*[A](s: OrderedSet[A]): int {.inline.} =
## Alias for `len() <#len,OrderedSet[A]>`_.
##
## Card stands for the `cardinality
## <http://en.wikipedia.org/wiki/Cardinality>`_ of a set.
result = s.counter
proc `==`*[A](s, t: OrderedSet[A]): bool =
## Equality for ordered sets.
runnableExamples:
let
a = toOrderedSet([1, 2])
b = toOrderedSet([2, 1])
assert(not (a == b))
if s.counter != t.counter: return false
var h = s.first
var g = t.first
var compared = 0
while h >= 0 and g >= 0:
var nxh = s.data[h].next
var nxg = t.data[g].next
if isFilled(s.data[h].hcode) and isFilled(t.data[g].hcode):
if s.data[h].key == t.data[g].key:
inc compared
else:
return false
h = nxh
g = nxg
result = compared == s.counter
proc hash*[A](s: OrderedSet[A]): Hash =
## Hashing of OrderedSet.
forAllOrderedPairs:
result = result !& s.data[h].hcode
result = !$result
proc `$`*[A](s: OrderedSet[A]): string =
## Converts the ordered hash set `s` to a string, mostly for logging and
## printing purposes.
##
## Don't use this proc for serialization, the representation may change at
## any moment and values are not escaped.
##
## **Examples:**
##
## .. code-block::
## echo toOrderedSet([2, 4, 5])
## # --> {2, 4, 5}
## echo toOrderedSet(["no", "esc'aping", "is \" provided"])
## # --> {no, esc'aping, is " provided}
dollarImpl()
iterator items*[A](s: OrderedSet[A]): A =
## Iterates over keys in the ordered set `s` in insertion order.
##
## If you need a sequence with the elements you can use `sequtils.toSeq
## template <sequtils.html#toSeq.t,untyped>`_.
##
## .. code-block::
## var a = initOrderedSet[int]()
## for value in [9, 2, 1, 5, 1, 8, 4, 2]:
## a.incl(value)
## for value in a.items:
## echo "Got ", value
## # --> Got 9
## # --> Got 2
## # --> Got 1
## # --> Got 5
## # --> Got 8
## # --> Got 4
forAllOrderedPairs:
yield s.data[h].key
iterator pairs*[A](s: OrderedSet[A]): tuple[a: int, b: A] =
## Iterates through (position, value) tuples of OrderedSet `s`.
runnableExamples:
let a = toOrderedSet("abracadabra")
var p = newSeq[(int, char)]()
for x in pairs(a):
p.add(x)
assert p == @[(0, 'a'), (1, 'b'), (2, 'r'), (3, 'c'), (4, 'd')]
forAllOrderedPairs:
yield (idx, s.data[h].key)
proc isValid*[A](s: OrderedSet[A]): bool {.deprecated:
"Deprecated since v0.20; sets are initialized by default".} =
##
## Returns `true` if the set has been initialized (with `initHashSet proc
## <#initOrderedSet,int>`_ or `init proc <#init,OrderedSet[A],int>`_).
##
## **Examples:**
##
## .. code-block ::
## proc savePreferences(options: OrderedSet[string]) =
## assert options.isValid, "Pass an initialized set!"
## # Do stuff here, may crash in release builds!
result = s.data.len > 0
# -----------------------------------------------------------------------
when isMainModule and not defined(release):
proc testModule() =
## Internal micro test to validate docstrings and such.
block isValidTest: # isValid is deprecated
var options: HashSet[string]
proc savePreferences(options: HashSet[string]) =
assert options.isValid, "Pass an initialized set!"
options = initHashSet[string]()
options.savePreferences
block lenTest:
var values: HashSet[int]
assert(not values.isValid)
assert values.len == 0
assert values.card == 0
block setIterator:
type pair = tuple[a, b: int]
var a, b = initHashSet[pair]()
a.incl((2, 3))
a.incl((3, 2))
a.incl((2, 3))
for x, y in a.items:
b.incl((x - 2, y + 1))
assert a.len == b.card
assert a.len == 2
#echo b
block setContains:
var values = initHashSet[int]()
assert(not values.contains(2))
values.incl(2)
assert values.contains(2)
values.excl(2)
assert(not values.contains(2))
values.incl(4)
var others = toHashSet([6, 7])
values.incl(others)
assert values.len == 3
values.init
assert values.containsOrIncl(2) == false
assert values.containsOrIncl(2) == true
var
a = toHashSet([1, 2])
b = toHashSet([1])
b.incl(2)
assert a == b
block exclusions:
var s = toHashSet([2, 3, 6, 7])
s.excl(2)
s.excl(2)
assert s.len == 3
var
numbers = toHashSet([1, 2, 3, 4, 5])
even = toHashSet([2, 4, 6, 8])
numbers.excl(even)
#echo numbers
# --> {1, 3, 5}
block toSeqAndString:
var a = toHashSet([2, 7, 5])
var b = initHashSet[int](rightSize(a.len))
for x in [2, 7, 5]: b.incl(x)
assert($a == $b)
#echo a
#echo toHashSet(["no", "esc'aping", "is \" provided"])
#block orderedToSeqAndString:
# echo toOrderedSet([2, 4, 5])
# echo toOrderedSet(["no", "esc'aping", "is \" provided"])
block setOperations:
var
a = toHashSet(["a", "b"])
b = toHashSet(["b", "c"])
c = union(a, b)
assert c == toHashSet(["a", "b", "c"])
var d = intersection(a, b)
assert d == toHashSet(["b"])
var e = difference(a, b)
assert e == toHashSet(["a"])
var f = symmetricDifference(a, b)
assert f == toHashSet(["a", "c"])
assert d < a and d < b
assert((a < a) == false)
assert d <= a and d <= b
assert((a <= a))
# Alias test.
assert a + b == toHashSet(["a", "b", "c"])
assert a * b == toHashSet(["b"])
assert a - b == toHashSet(["a"])
assert a -+- b == toHashSet(["a", "c"])
assert disjoint(a, b) == false
assert disjoint(a, b - a) == true
block mapSet:
var a = toHashSet([1, 2, 3])
var b = a.map(proc (x: int): string = $x)
assert b == toHashSet(["1", "2", "3"])
block isValidTest: # isValid is deprecated
var cards: OrderedSet[string]
proc saveTarotCards(cards: OrderedSet[string]) =
assert cards.isValid, "Pass an initialized set!"
cards = initOrderedSet[string]()
cards.saveTarotCards
block lenTest:
var values: OrderedSet[int]
assert(not values.isValid)
assert values.len == 0
assert values.card == 0
block setIterator:
type pair = tuple[a, b: int]
var a, b = initOrderedSet[pair]()
a.incl((2, 3))
a.incl((3, 2))
a.incl((2, 3))
for x, y in a.items:
b.incl((x - 2, y + 1))
assert a.len == b.card
assert a.len == 2
block setPairsIterator:
var s = toOrderedSet([1, 3, 5, 7])
var items = newSeq[tuple[a: int, b: int]]()
for idx, item in s: items.add((idx, item))
assert items == @[(0, 1), (1, 3), (2, 5), (3, 7)]
block exclusions:
var s = toOrderedSet([1, 2, 3, 6, 7, 4])
s.excl(3)
s.excl(3)
s.excl(1)
s.excl(4)
var items = newSeq[int]()
for item in s: items.add item
assert items == @[2, 6, 7]
block: #9005
var s = initOrderedSet[(int, int)]()
for i in 0 .. 30: incl(s, (i, 0))
for i in 0 .. 30: excl(s, (i, 0))
doAssert s.len == 0
#block orderedSetIterator:
# var a = initOrderedSet[int]()
# for value in [9, 2, 1, 5, 1, 8, 4, 2]:
# a.incl(value)
# for value in a.items:
# echo "Got ", value
block setContains:
var values = initOrderedSet[int]()
assert(not values.contains(2))
values.incl(2)
assert values.contains(2)
block toSeqAndString:
var a = toOrderedSet([2, 4, 5])
var b = initOrderedSet[int]()
for x in [2, 4, 5]: b.incl(x)
assert($a == $b)
assert(a == b) # https://github.com/Araq/Nim/issues/1413
block initBlocks:
var a: OrderedSet[int]
a.init(4)
a.incl(2)
a.init
assert a.len == 0 and a.isValid
a = initOrderedSet[int](4)
a.incl(2)
assert a.len == 1
var b: HashSet[int]
b.init(4)
b.incl(2)
b.init
assert b.len == 0 and b.isValid
b = initHashSet[int](4)
b.incl(2)
assert b.len == 1
block:
type FakeTable = object
dataLen: int
counter: int
countDeleted: int
var t: FakeTable
for i in 0 .. 32:
var s = rightSize(i)
t.dataLen = s
t.counter = i
doAssert s > i and not mustRehash(t),
"performance issue: rightSize() will not elide enlarge() at: " & $i
block missingOrExcl:
var s = toOrderedSet([2, 3, 6, 7])
assert s.missingOrExcl(4) == true
assert s.missingOrExcl(6) == false
block orderedSetEquality:
type pair = tuple[a, b: int]
var aa = initOrderedSet[pair]()
var bb = initOrderedSet[pair]()
var x = (a: 1, b: 2)
var y = (a: 3, b: 4)
aa.incl(x)
aa.incl(y)
bb.incl(x)
bb.incl(y)
assert aa == bb
block setsWithoutInit:
var
a: HashSet[int]
b: HashSet[int]
c: HashSet[int]
d: HashSet[int]
e: HashSet[int]
doAssert a.containsOrIncl(3) == false
doAssert a.contains(3)
doAssert a.len == 1
doAssert a.containsOrIncl(3)
a.incl(3)
doAssert a.len == 1
a.incl(6)
doAssert a.len == 2
b.incl(5)
doAssert b.len == 1
b.excl(5)
b.excl(c)
doAssert b.missingOrExcl(5)
doAssert b.disjoint(c)
d = b + c
doAssert d.len == 0
d = b * c
doAssert d.len == 0
d = b - c
doAssert d.len == 0
d = b -+- c
doAssert d.len == 0
doAssert (d < e) == false
doAssert d <= e
doAssert d == e
block setsWithoutInit:
var
a: OrderedSet[int]
b: OrderedSet[int]
c: OrderedSet[int]
d: HashSet[int]
doAssert a.containsOrIncl(3) == false
doAssert a.contains(3)
doAssert a.len == 1
doAssert a.containsOrIncl(3)
a.incl(3)
doAssert a.len == 1
a.incl(6)
doAssert a.len == 2
b.incl(5)
doAssert b.len == 1
doAssert b.missingOrExcl(5) == false
doAssert b.missingOrExcl(5)
doAssert c.missingOrExcl(9)
d.incl(c)
doAssert d.len == 0
when not defined(testing):
echo "Micro tests run successfully."
testModule()