A hash map from character(len=*) keys to integer(int64) values whose
iteration order is a documented, reproducible guarantee rather than an
implementation accident.
## The iteration-order guarantee
Iteration is always in insertion order. keys(), values() and
at() visit the entries in the exact order in which their keys were
first inserted. That order does not depend on the bucket count, on
how many times the table has grown and rehashed, on the hash function,
or on which other keys were inserted or removed. Two runs performing
the same sequence of insert/remove calls observe the same iteration
order, on every supported compiler.
The rules that make that guarantee precise:
insert on a key that is not present appends the entry at the end
of the iteration order.insert on a key that is present overwrites the value and leaves
the key exactly where it was in the iteration order. It is an update,
not a re-insertion.remove deletes the entry from the iteration order. Every remaining
entry keeps its relative position: entries before the removed one are
untouched, entries after it shift down by one index.remove followed by insert of the same key is a fresh insertion:
the key reappears at the end of the iteration order, not at its old
position.## Key identity
Keys are compared by exact length and exact characters, not with the
Fortran == operator. Fortran blank-pads the shorter operand when
comparing character values, so "a" == "a " is .true. in the
language; in this map "a" and "a " are different keys. Trailing
blanks are significant and are stored verbatim.
Beware that a character(len=8) variable holding "a" is the eight
character key "a ", because that is what the actual argument
contains. Pass trim(name) if trailing blanks are not meant to count.
The zero-length key "" is a perfectly ordinary key.
## Capacity, load factor and growth
The bucket table is a power of two, at least 8 buckets, addressed by
open addressing with linear probing. It grows by doubling whenever a
new entry would push occupancy above a load factor of 0.75. There
are no tombstones: remove compacts the insertion-order table and
rebuilds the bucket table from scratch, so remove costs
O(size() + bucket_count()) while insert, get and has_key are
amortised O(1). This map is built for reproducible iteration and
simple, portable code, not for remove-heavy workloads.
## Example
type(hash_map_t) :: counts
character(len=:), allocatable :: k
integer(int64) :: v
integer(default_int) :: i
call counts%init(16_default_int)
call counts%insert("beta", 2_int64)
call counts%insert("alpha", 1_int64)
call counts%insert("beta", 20_int64) ! updates, stays at position 1
do i = 1, counts%size()
call counts%at(i, k, v) ! "beta" then "alpha"
end do
call counts%destroy()
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer(kind=default_int), | private, | parameter | :: | BUCKET_EMPTY | = | 0 |
Sentinel stored in a bucket that does not hold an entry. |
| integer(kind=default_int), | private, | parameter | :: | LOAD_DEN | = | 4 |
Denominator of the maximum load factor (3/4). |
| integer(kind=default_int), | private, | parameter | :: | LOAD_NUM | = | 3 |
Numerator of the maximum load factor (3/4). |
| integer(kind=default_int), | private, | parameter | :: | MIN_BUCKETS | = | 8 |
Smallest bucket table this map will ever allocate. |
| integer(kind=default_int), | private, | parameter | :: | MIN_ENTRIES | = | 8 |
Smallest insertion-order table this map will ever allocate. |
| integer(kind=default_int), | private, | parameter | :: | MIN_POOL | = | 64 |
Smallest key character pool this map will ever allocate. |
Insertion-ordered hash map: character(len=*) keys, integer(int64)
values. Default initialisation yields a valid empty map; the first
insert allocates with default capacity if init was never called.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer(kind=default_int), | private, | allocatable | :: | buckets(:) |
Bucket table; |
||
| integer(kind=default_int), | private | :: | entry_cap | = | 0 |
Allocated length of the entry arrays. |
|
| integer(kind=int64), | private, | allocatable | :: | entry_value(:) |
Value of each entry, in insertion order. |
||
| character(len=1), | private, | allocatable | :: | key_pool(:) |
Packed storage for all key characters. |
||
| integer(kind=default_int), | private, | allocatable | :: | key_size(:) |
Length in characters of each key, in insertion order. |
||
| integer(kind=default_int), | private, | allocatable | :: | key_start(:) |
First character of each key inside |
||
| integer(kind=default_int), | private | :: | n_buckets | = | 0 |
Number of buckets, always a power of two, 0 while unallocated. |
|
| integer(kind=default_int), | private | :: | n_entries | = | 0 |
Number of live entries, i.e. the value returned by |
|
| integer(kind=default_int), | private | :: | pool_cap | = | 0 |
Allocated length of |
|
| integer(kind=default_int), | private | :: | pool_used | = | 0 |
Number of characters currently used in |
| procedure, public :: at => hash_map_at | |
| procedure, public :: bucket_count => hash_map_bucket_count | |
| procedure, public :: clear => hash_map_clear | |
| procedure, public :: destroy => hash_map_destroy | |
| procedure, public :: get => hash_map_get | |
| procedure, public :: has_key => hash_map_has_key | |
| procedure, public :: init => hash_map_init | |
| procedure, public :: insert => hash_map_insert | |
| procedure, public :: is_empty => hash_map_is_empty | |
| procedure, public :: keys => hash_map_keys | |
| procedure, public :: remove => hash_map_remove | |
| procedure, public :: size => hash_map_size | |
| procedure, public :: values => hash_map_values |
Rebuild the key of an entry from the packed character pool.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | entry |
Index of key in the insertion-order table, or 0 when absent.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| character(len=*), | intent(in) | :: | key |
Current number of buckets, 0 while the map is unallocated. Exposed so that callers and tests can observe growth; it never affects iteration order.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this |
.true. when key is stored in the map.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| character(len=*), | intent(in) | :: | key |
.true. when the map holds no entries.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this |
All keys, in insertion order.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this |
Number of entries currently stored.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this |
All values, in insertion order. values() and keys() are index
aligned: values(i) belongs to keys(i). Zero-sized for an empty
map.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this |
Bucket a key hashes to before probing.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| character(len=*), | intent(in) | :: | key |
Exact key comparison: same length and same characters. Deliberately
not the Fortran == operator, which blank-pads the shorter operand.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | entry | |||
| character(len=*), | intent(in) | :: | key |
First empty bucket in the probe sequence of key. The load factor
keeps at least one bucket empty at all times, so this always finds
one.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| character(len=*), | intent(in) | :: | key |
Double the bucket table and rehash. Entry storage is never moved.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this |
Fetch the entry at position in the iteration order, counting from 1.
This is the exact-fidelity iteration accessor: key comes back with
the key’s true length, trailing blanks included.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | position | |||
| character(len=:), | intent(out), | allocatable | :: | key | ||
| integer(kind=int64), | intent(out) | :: | value | |||
| type(error_t), | intent(out), | optional | :: | err |
Remove every entry but keep the allocated capacity, so that refilling
the map does not have to reallocate. A cleared map iterates as empty
and the next insert starts a fresh insertion order at position 1.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this |
Release every buffer and return the map to the unallocated state.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this |
Look up key. A missing key is an ordinary outcome, not an error:
found comes back .false. and value is set to 0.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(in) | :: | this | |||
| character(len=*), | intent(in) | :: | key | |||
| integer(kind=int64), | intent(out) | :: | value | |||
| logical, | intent(out) | :: | found |
Allocate the map, discarding any entries it already held.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this | |||
| integer(kind=default_int), | intent(in), | optional | :: | initial_capacity |
Insert key with value, or overwrite the value of an existing key.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this | |||
| character(len=*), | intent(in) | :: | key |
Stored verbatim; trailing blanks are part of the key. |
||
| integer(kind=int64), | intent(in) | :: | value |
Remove key from the map.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this | |||
| character(len=*), | intent(in) | :: | key | |||
| type(error_t), | intent(out), | optional | :: | err |
Re-index every entry into the bucket table. Touches buckets only, so the insertion order is untouched by construction.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this |
Ensure the insertion-order arrays hold at least needed entries.
Explicit allocate/copy/move_alloc rather than
reallocation-on-assignment, which is shaky on some supported
compilers.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | needed |
Ensure the packed key pool holds at least needed characters.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(hash_map_t), | intent(inout) | :: | this | |||
| integer(kind=default_int), | intent(in) | :: | needed |