Odin by Example: Bit sets

Bit sets model the mathematical notion of a set as a packed bit vector, giving compact storage and fast operations. The element type is an enum or a range, and every element maps to one bit.

Declare a bit set with bit_set[...]. Create values with set literals like {.North, .West} or {'A', 'Z'}. The zero value is an empty set {}.

Set operations use the usual operators: | union, & intersection, - difference, ~ symmetric difference, and <=/>= for subset/superset checks. Membership is tested with `in` and `not_in`, and card() counts elements.

Bit sets are a cleaner way to express flags than integer constants that are bitwise-or-ed together.

$ odin run bit-sets.odin -file
a: bit_set[Direction]{North, West}
b: bit_set[Direction]{East, West}
union: bit_set[Direction]{North, East, West}
intersection: bit_set[Direction]{West}
difference: bit_set[Direction]{North}
symmetric difference: bit_set[Direction]{North, East}
North in a: true
East in a: false
cardinality of a: 2
a equals {West, North}: true
a subset of {N,W,S}: true
char set: bit_set['A'..='Z']{65, 67, 90}
'B' in cs: false
'Z' in cs: true
size of Char_Set: 4
size of Big_Set: 16
size of Small_Explicit: 1
permissions after edits: bit_set[Direction]{West}
package main

import "core:fmt"

// Element type can be an enum...
Direction :: enum {North, East, South, West}
Direction_Set :: bit_set[Direction]

// ...or a range, including a character range.
Char_Set :: bit_set['A'..='Z']
Big_Set :: bit_set[0..<128]

// The backing integer type can be chosen explicitly; it must
// cover the range. A u8 holds at most 8 elements.
Small_Explicit :: bit_set[0..<8; u8]

main :: proc() {
	a := Direction_Set{.North, .West}
	b := Direction_Set{.East, .West}

	fmt.println("a:", a)
	fmt.println("b:", b)

	// Set operations combine two sets.
	fmt.println("union:", a | b)
	fmt.println("intersection:", a & b)
	fmt.println("difference:", a - b)
	fmt.println("symmetric difference:", a ~ b)

	// Membership tests use `in` and `not_in`.
	fmt.println("North in a:", .North in a)
	fmt.println("East in a:", .East in a)

	// card() counts the elements present.
	fmt.println("cardinality of a:", card(a))

	// Sets compare by contents, regardless of literal order,
	// and subset/superset checks use the ordering operators.
	fmt.println("a equals {West, North}:", a == Direction_Set{.West, .North})
	fmt.println("a subset of {N,W,S}:", a <= Direction_Set{.North, .West, .South})

	// Character ranges work the same way; the underlying
	// values are printed as numbers.
	cs := Char_Set{'A', 'C', 'Z'}
	fmt.println("char set:", cs)
	fmt.println("'B' in cs:", 'B' in cs)
	fmt.println("'Z' in cs:", 'Z' in cs)

	// Sizes: 26 chars pack into 4 bytes, 128 elements into 16,
	// and the explicit u8 backing stays at 1 byte.
	fmt.println("size of Char_Set:", size_of(Char_Set))
	fmt.println("size of Big_Set:", size_of(Big_Set))
	fmt.println("size of Small_Explicit:", size_of(Small_Explicit))

	// += and -= add and remove single elements, handy for
	// accumulating flags.
	permissions := Direction_Set{}
	permissions += {.North}
	permissions += {.West}
	permissions -= {.North}
	fmt.println("permissions after edits:", permissions)
}

← bit-fields | unions →