Skip to content

Commit 12be8fe

Browse files
committed
Let dictionaries respect insertion order
1 parent 6dcb65e commit 12be8fe

File tree

7 files changed

+33
-23
lines changed

7 files changed

+33
-23
lines changed

docs/src/reference/types.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,9 @@ Return a new array with the same items, but sorted.
674674
A map from string keys to values.
675675

676676
You can construct a dictionary by enclosing comma-separated `key: value` pairs
677-
in parentheses. The values do not have to be of the same type.
677+
in parentheses. The values do not have to be of the same type. Since empty
678+
parentheses already yield an empty array, you have to use the special `(:)`
679+
syntax to create an empty dictionary.
678680

679681
A dictionary is conceptually similar to an array, but it is indexed by strings
680682
instead of integers. You can access and create dictionary entries with the
@@ -685,12 +687,8 @@ the value. Dictionaries can be added with the `+` operator and
685687
To check whether a key is present in the dictionary, use the `in` keyword.
686688

687689
You can iterate over the pairs in a dictionary using a
688-
[for loop]($scripting/#loops).
689-
Dictionaries are always ordered by key.
690-
691-
Since empty parentheses already yield an empty array, you have to use the
692-
special `(:)` syntax to create an empty dictionary.
693-
690+
[for loop]($scripting/#loops). This will iterate in the order the pairs were
691+
inserted / declared.
694692

695693
## Example
696694
```example
@@ -735,12 +733,12 @@ If the dictionary already contains this key, the value is updated.
735733
The value of the pair that should be inserted.
736734

737735
### keys()
738-
Returns the keys of the dictionary as an array in sorted order.
736+
Returns the keys of the dictionary as an array in insertion order.
739737

740738
- returns: array
741739

742740
### values()
743-
Returns the values of the dictionary as an array in key-order.
741+
Returns the values of the dictionary as an array in insertion order.
744742

745743
- returns: array
746744

src/eval/dict.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use std::collections::BTreeMap;
21
use std::fmt::{self, Debug, Formatter};
2+
use std::hash::{Hash, Hasher};
33
use std::ops::{Add, AddAssign};
44
use std::sync::Arc;
55

@@ -16,7 +16,7 @@ use crate::util::{pretty_array_like, separated_list, ArcExt};
1616
macro_rules! __dict {
1717
($($key:expr => $value:expr),* $(,)?) => {{
1818
#[allow(unused_mut)]
19-
let mut map = std::collections::BTreeMap::new();
19+
let mut map = $crate::eval::IndexMap::new();
2020
$(map.insert($key.into(), $value.into());)*
2121
$crate::eval::Dict::from_map(map)
2222
}};
@@ -25,9 +25,12 @@ macro_rules! __dict {
2525
#[doc(inline)]
2626
pub use crate::__dict as dict;
2727

28+
#[doc(inline)]
29+
pub use indexmap::IndexMap;
30+
2831
/// A reference-counted dictionary with value semantics.
29-
#[derive(Default, Clone, PartialEq, Hash)]
30-
pub struct Dict(Arc<BTreeMap<Str, Value>>);
32+
#[derive(Default, Clone, PartialEq)]
33+
pub struct Dict(Arc<IndexMap<Str, Value>>);
3134

3235
impl Dict {
3336
/// Create a new, empty dictionary.
@@ -36,7 +39,7 @@ impl Dict {
3639
}
3740

3841
/// Create a new dictionary from a mapping of strings to values.
39-
pub fn from_map(map: BTreeMap<Str, Value>) -> Self {
42+
pub fn from_map(map: IndexMap<Str, Value>) -> Self {
4043
Self(Arc::new(map))
4144
}
4245

@@ -116,7 +119,7 @@ impl Dict {
116119
}
117120

118121
/// Iterate over pairs of references to the contained keys and values.
119-
pub fn iter(&self) -> std::collections::btree_map::Iter<Str, Value> {
122+
pub fn iter(&self) -> indexmap::map::Iter<Str, Value> {
120123
self.0.iter()
121124
}
122125

@@ -171,6 +174,15 @@ impl AddAssign for Dict {
171174
}
172175
}
173176

177+
impl Hash for Dict {
178+
fn hash<H: Hasher>(&self, state: &mut H) {
179+
state.write_usize(self.0.len());
180+
for item in self {
181+
item.hash(state);
182+
}
183+
}
184+
}
185+
174186
impl Extend<(Str, Value)> for Dict {
175187
fn extend<T: IntoIterator<Item = (Str, Value)>>(&mut self, iter: T) {
176188
Arc::make_mut(&mut self.0).extend(iter);
@@ -185,7 +197,7 @@ impl FromIterator<(Str, Value)> for Dict {
185197

186198
impl IntoIterator for Dict {
187199
type Item = (Str, Value);
188-
type IntoIter = std::collections::btree_map::IntoIter<Str, Value>;
200+
type IntoIter = indexmap::map::IntoIter<Str, Value>;
189201

190202
fn into_iter(self) -> Self::IntoIter {
191203
Arc::take(self.0).into_iter()
@@ -194,7 +206,7 @@ impl IntoIterator for Dict {
194206

195207
impl<'a> IntoIterator for &'a Dict {
196208
type Item = (&'a Str, &'a Value);
197-
type IntoIter = std::collections::btree_map::Iter<'a, Str, Value>;
209+
type IntoIter = indexmap::map::Iter<'a, Str, Value>;
198210

199211
fn into_iter(self) -> Self::IntoIter {
200212
self.iter()

src/eval/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub use self::value::*;
3737

3838
pub(crate) use self::methods::methods_on;
3939

40-
use std::collections::{BTreeMap, HashSet};
40+
use std::collections::HashSet;
4141
use std::mem;
4242
use std::path::{Path, PathBuf};
4343

@@ -870,7 +870,7 @@ impl Eval for ast::Dict {
870870
type Output = Dict;
871871

872872
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
873-
let mut map = BTreeMap::new();
873+
let mut map = indexmap::IndexMap::new();
874874

875875
for item in self.items() {
876876
match item {

src/eval/value.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,6 @@ mod tests {
444444
test(array![1, 2], "(1, 2)");
445445
test(dict![], "(:)");
446446
test(dict!["one" => 1], "(one: 1)");
447-
test(dict!["two" => false, "one" => 1], "(one: 1, two: false)");
447+
test(dict!["two" => false, "one" => 1], "(two: false, one: 1)");
448448
}
449449
}

tests/ref/compiler/for.png

-10 Bytes
Loading

tests/typ/compiler/dict.typ

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
#let dict = (a: 3, c: 2, b: 1)
4949
#test("c" in dict, true)
5050
#test(dict.len(), 3)
51-
#test(dict.values(), (3, 1, 2))
52-
#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3b1c2")
51+
#test(dict.values(), (3, 2, 1))
52+
#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3c2b1")
5353

5454
#dict.remove("c")
5555
#test("c" in dict, false)

tests/typ/compiler/for.typ

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Empty array.
88
#for x in () [Nope]
99

10-
// Dictionary is not traversed in insertion order.
10+
// Dictionary is traversed in insertion order.
1111
// Should output `Age: 2. Name: Typst.`.
1212
#for (k, v) in (Name: "Typst", Age: 2) [
1313
#k: #v.

0 commit comments

Comments
 (0)