fredderic's dict

The following is a few extensions to the dict command which I have found useful. (The proc-ensemble command is the one shown on my fredderic's gems page.) Of the lot shown here, the two get variants are by far the most useful.

dict get? dictionary key ?...? varName

If key exists within the dictionary, then store the value in varName and return true. Otherwise, returns false and leaves the variable unchanged.

dict get! dictionary key ?...? default

If key exists within the dictionary, then return it as usual. Otherwise, return the supplied default value.

dict set? dictName key ?...? value varName

Sets a new value in the dictionary, but returns any existing value in the manner of dict get? as well.

dict set! dictName key ?...? value

Sets a new value in the dictionary, but only if no value for that key already exists. (Not particularly useful, but it's had its moments.)

dict unset? dictName key ?...? varName

Unsets a key from the dictionary, but returns any existing value in the manner of dict get? as well.

dict unset! dictName key ?...?

Mostly for symmetry, this one simply doesn't error if the key does not exist.

dict lappend! dictName key ?...? value

Intended primarily for dictionary keys containing lists of flags, this command won't append the given value to a key which already contains it.

dict multiset dictionary ?key ...? itemList key value

Sets the specified key / value pair over each of the sub-keys listed in itemList.

dict zip ?key dictionary? ...

Merge several dictionaries, inserting an extra level of nesting to distinguish the values from each dictionary.


proc-ensemble ::dict {
        get? {args} {
                # dict::get to variable and return boolean
                if { [llength $args] < 3 } {
                        error-args "dict get? dictionary key ?...? varName"
                }
                ::set varName [lindex $args end]
                ::set args [lrange $args 0 end-1]
                if { [llength [lindex $args 0]] % 2 == 1 } {
                        return -level 1 -code error \
                                "missing value to go with key"
                }

                if { [dict exists {*}$args] } {
                        uplevel 1 [list ::set $varName [dict get {*}$args]]
                        return 1
                }
                return 0
        }

        get! {args} {
                # dict::get with default return value
                if { [llength $args] < 3 } {
                        error-args "dict get! dictionary key ?...? default"
                }
                ::set default [lindex $args end]
                ::set args [lrange $args 0 end-1]
                if { [llength [lindex $args 0]] % 2 == 1 } {
                        return -level 1 -code error \
                                "missing value to go with key"
                }
        
                if { [dict exists {*}$args] } {
                        return [dict get {*}$args]
                } else {
                        return $default
                }
        }

        set? {args} {
                # dict::set but return previous value
                if { [llength $args] < 4 } {
                        error-args "dict set? varName key ?...? value varName"
                }
                upvar 1 [lindex $args 0] dict [lindex $args end] var
                ::set path [lrange $args 1 end-2]
                ::set valu [lindex $args end-1]
                if { [::info exists dict] && [dict exists $dict {*}$path] } {
                        ::set var [dict get $dict {*}$path]
                        ::set rc 1
                } else {
                        ::set rc 0
                }
                dict ::set dict {*}$path $valu
                return $rc
        }

        set! {args} {
                # dict::set but only if element doesn't exist
                if { [llength $args] < 3 } {
                        error-args "dict set! varName key ?...? value"
                }
                upvar 1 [lindex $args 0] var
                ::set args [lrange $args 1 end]
                if { ! [::info exists var] ||
                     ! [dict exists $var {*}[lrange $args 0 end-1]] } {
                        dict set var {*}$args
                }
                return $var
        }

        unset? {args} {
                # dict::unset but returns boolean and old value
                if { [llength $args] < 3 } {
                        error-args "dict unset? varName key ?...? retVar"
                }
                upvar 1 [lindex $args 0] var [lindex $args end] ret
                ::set args [lrange $args 1 end-1]
                if { [::info exists var] && [dict exists $var {*}$args] } {
                        ::set ret [dict get $var {*}$args]
                        dict unset var {*}$args
                        return 1
                }
                return 0
        }

        unset! {args} {
                # dict::unset but doesn't throw error
                if { [llength $args] < 2 } {
                        error-args "dict unset! varName key ?...?"
                }
                upvar 1 [lindex $args 0] var
                ::set args [lrange $args 1 end]
                if { ! [::info exists var] || ! [dict exists $var {*}$args] } return
                return [dict unset var {*}$args]
        }

        lappend! {args} {
                # dict::append but only if list element doesn't exist
                if { [llength $args] < 3 } {
                        error-args "dict lappend! varName key ?...? value"
                }
                upvar 1 [lindex $args 0] var
                ::set value [lindex $args end]
                ::set args [lrange $args 1 end-1]
                if { [::info exists var] && [dict exists $var {*}$args] } {
                        ::set list [dict get $var {*}$args]
                        if { $value in $list } {return $var}
                        lappend list $value
                } else {
                        ::set list [list $value]
                }
                return [dict set var {*}$args $list]
        }

        multiset {args} {
                # dict::set that sets the same key/value over multiple sub-keys
                if { [llength $args] < 4 } {
                        error-args "dict multiset dictionary ?key ...? itemList key value"
                }
                ::set keys [lassign [lrange $args 0 end-3] dictionary]
                lassign [lrange $args end-2 end] items key value
                # now process the multiset
                ::set dict [dict get $dictionary {*}$keys]
                foreach item $items {
                        dict set dict $item $key $value
                }
                if { [llength $keys] == 0 } {return $dict}
                return [dict set dictionary {*}$keys $dict]
        }

        zip {args} {
                # dict::merge that adds a level inbetween
                if { [llength $args] % 2 == 1 } {
                        error-args "dict zip ?key dictionary? ..."
                }
                ::set dict [list]
                foreach {key items} $args {
                        dict for {name item} $items {
                                dict set dict $name $key $item
                        }
                }
                return $dict
        }
}