1# prioqueue.tcl --
2#
3#  Priority Queue implementation for Tcl.
4#
5# adapted from queue.tcl
6# Copyright (c) 2002,2003 Michael Schlenker
7# Copyright (c) 2008 Alejandro Paz <vidriloco@gmail.com>
8#
9# See the file "license.terms" for information on usage and redistribution
10# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
11#
12# RCS: @(#) $Id: prioqueue.tcl,v 1.10 2008/09/04 04:35:02 andreas_kupries Exp $
13
14package require Tcl 8.2
15
16namespace eval ::struct {}
17
18namespace eval ::struct::prioqueue {
19    # The queues array holds all of the queues you've made
20    variable queues
21
22    # counter is used to give a unique name for unnamed queues
23    variable counter 0
24
25    # commands is the list of subcommands recognized by the queue
26    variable commands [list \
27        "clear" \
28        "destroy"   \
29        "get"   \
30        "peek"  \
31        "put"   \
32        "remove" \
33        "size"  \
34        "peekpriority" \
35        ]
36
37    variable sortopt [list \
38        "-integer" \
39        "-real" \
40        "-ascii" \
41        "-dictionary" \
42        ]
43
44    # this is a simple design decision, that integer and real
45    # are sorted decreasing (-1), and -ascii and -dictionary are sorted -increasing (1)
46    # the values here map to the sortopt list
47    # could be changed to something configurable.
48    variable sortdir [list \
49        "-1" \
50        "-1" \
51        "1" \
52        "1" \
53        ]
54
55
56
57    # Only export one command, the one used to instantiate a new queue
58    namespace export prioqueue
59
60    proc K {x y} {set x} ;# DKF's K combinator
61}
62
63# ::struct::prioqueue::prioqueue --
64#
65#   Create a new prioqueue with a given name; if no name is given, use
66#   prioqueueX, where X is a number.
67#
68# Arguments:
69#   sorting sorting option for lsort to use, no -command option
70#           defaults to integer
71#   name    name of the queue; if null, generate one.
72#           names may not begin with -
73#
74#
75# Results:
76#   name    name of the queue created
77
78proc ::struct::prioqueue::prioqueue {args} {
79    variable queues
80    variable counter
81    variable queues_sorting
82    variable sortopt
83
84    # check args
85    if {[llength $args] > 2} {
86        error "wrong # args: should be \"[lindex [info level 0] 0] ?-ascii|-dictionary|-integer|-real? ?name?\""
87    }
88    if {[llength $args] == 0} {
89        # defaulting to integer priorities
90        set sorting -integer
91    } else {
92        if {[llength $args] == 1} {
93            if {[string match "-*" [lindex $args 0]]==1} {
94                set sorting [lindex $args 0]
95            } else {
96                set sorting -integer
97                set name [lindex $args 0]
98            }
99        } else {
100            if {[llength $args] == 2} {
101                foreach {sorting name} $args {break}
102            }
103        }
104    }
105    # check option (like lsort sorting options without -command)
106    if {[lsearch $sortopt $sorting] == -1} {
107        # if sortoption is unknown, but name is a sortoption we give a better error message
108        if {[info exists name] && [lsearch $sortopt $name]!=-1} {
109            error "wrong argument position: should be \"[lindex [info level 0] 0] ?-ascii|-dictionary|-integer|-real? ?name?\""
110        }
111        error "unknown sort option \"$sorting\""
112    }
113    # create name if not given
114    if {![info exists name]} {
115        incr counter
116        set name "prioqueue${counter}"
117    }
118
119    if { ![string equal [info commands ::$name] ""] } {
120    error "command \"$name\" already exists, unable to create prioqueue"
121    }
122
123    # Initialize the queue as empty
124    set queues($name) [list ]
125    switch -exact -- $sorting {
126    -integer { set queues_sorting($name) 0}
127    -real    { set queues_sorting($name) 1}
128    -ascii   { set queues_sorting($name) 2}
129    -dictionary { set queues_sorting($name) 3}
130    }
131
132    # Create the command to manipulate the queue
133    interp alias {} ::$name {} ::struct::prioqueue::QueueProc $name
134
135    return $name
136}
137
138##########################
139# Private functions follow
140
141# ::struct::prioqueue::QueueProc --
142#
143#   Command that processes all queue object commands.
144#
145# Arguments:
146#   name    name of the queue object to manipulate.
147#   args    command name and args for the command
148#
149# Results:
150#   Varies based on command to perform
151
152proc ::struct::prioqueue::QueueProc {name {cmd ""} args} {
153    # Do minimal args checks here
154    if { [llength [info level 0]] == 2 } {
155    error "wrong # args: should be \"$name option ?arg arg ...?\""
156    }
157
158    # Split the args into command and args components
159    if { [string equal [info commands ::struct::prioqueue::_$cmd] ""] } {
160    variable commands
161    set optlist [join $commands ", "]
162    set optlist [linsert $optlist "end-1" "or"]
163    error "bad option \"$cmd\": must be $optlist"
164    }
165    return [eval [linsert $args 0 ::struct::prioqueue::_$cmd $name]]
166}
167
168# ::struct::prioqueue::_clear --
169#
170#   Clear a queue.
171#
172# Arguments:
173#   name    name of the queue object.
174#
175# Results:
176#   None.
177
178proc ::struct::prioqueue::_clear {name} {
179    variable queues
180    set queues($name) [list]
181    return
182}
183
184# ::struct::prioqueue::_destroy --
185#
186#   Destroy a queue object by removing it's storage space and
187#   eliminating it's proc.
188#
189# Arguments:
190#   name    name of the queue object.
191#
192# Results:
193#   None.
194
195proc ::struct::prioqueue::_destroy {name} {
196    variable queues
197    variable queues_sorting
198    unset queues($name)
199    unset queues_sorting($name)
200    interp alias {} ::$name {}
201    return
202}
203
204# ::struct::prioqueue::_get --
205#
206#   Get an item from a queue.
207#
208# Arguments:
209#   name    name of the queue object.
210#   count   number of items to get; defaults to 1
211#
212# Results:
213#   item    first count items from the queue; if there are not enough
214#           items in the queue, throws an error.
215#
216
217proc ::struct::prioqueue::_get {name {count 1}} {
218    variable queues
219    if { $count < 1 } {
220    error "invalid item count $count"
221    }
222
223    if { $count > [llength $queues($name)] } {
224    error "insufficient items in prioqueue to fill request"
225    }
226
227    if { $count == 1 } {
228    # Handle this as a special case, so single item gets aren't listified
229    set item [lindex [lindex $queues($name) 0] 1]
230    set queues($name) [lreplace [K $queues($name) [set queues($name) ""]] 0 0]
231    return $item
232    }
233
234    # Otherwise, return a list of items
235    incr count -1
236    set items [lrange $queues($name) 0 $count]
237    foreach item $items {
238        lappend result [lindex $item 1]
239    }
240    set items ""
241
242    set queues($name) [lreplace [K $queues($name) [set queues($name) ""]] 0 $count]
243    return $result
244}
245
246# ::struct::prioqueue::_peek --
247#
248#   Retrive the value of an item on the queue without removing it.
249#
250# Arguments:
251#   name    name of the queue object.
252#   count   number of items to peek; defaults to 1
253#
254# Results:
255#   items   top count items from the queue; if there are not enough items
256#       to fufill the request, throws an error.
257
258proc ::struct::prioqueue::_peek {name {count 1}} {
259    variable queues
260    if { $count < 1 } {
261    error "invalid item count $count"
262    }
263
264    if { $count > [llength $queues($name)] } {
265    error "insufficient items in prioqueue to fill request"
266    }
267
268    if { $count == 1 } {
269    # Handle this as a special case, so single item pops aren't listified
270    return [lindex [lindex $queues($name) 0] 1]
271    }
272
273    # Otherwise, return a list of items
274    set index [expr {$count - 1}]
275    foreach item [lrange $queues($name) 0 $index] {
276        lappend result [lindex $item 1]
277    }
278    return $result
279}
280
281# ::struct::prioqueue::_peekpriority --
282#
283#   Retrive the priority of an item on the queue without removing it.
284#
285# Arguments:
286#   name    name of the queue object.
287#   count   number of items to peek; defaults to 1
288#
289# Results:
290#   items   top count items from the queue; if there are not enough items
291#       to fufill the request, throws an error.
292
293proc ::struct::prioqueue::_peekpriority {name {count 1}} {
294    variable queues
295    if { $count < 1 } {
296    error "invalid item count $count"
297    }
298
299    if { $count > [llength $queues($name)] } {
300    error "insufficient items in prioqueue to fill request"
301    }
302
303    if { $count == 1 } {
304    # Handle this as a special case, so single item pops aren't listified
305    return [lindex [lindex $queues($name) 0] 0]
306    }
307
308    # Otherwise, return a list of items
309    set index [expr {$count - 1}]
310    foreach item [lrange $queues($name) 0 $index] {
311        lappend result [lindex $item 0]
312    }
313    return $result
314}
315
316
317# ::struct::prioqueue::_put --
318#
319#   Put an item into a queue.
320#
321# Arguments:
322#   name    name of the queue object
323#   args    list of the form "item1 prio1 item2 prio2 item3 prio3"
324#
325# Results:
326#   None.
327
328proc ::struct::prioqueue::_put {name args} {
329    variable queues
330    variable queues_sorting
331    variable sortopt
332    variable sortdir
333
334    if { [llength $args] == 0 || [llength $args] % 2} {
335    error "wrong # args: should be \"$name put item prio ?item prio ...?\""
336    }
337
338    # check for prio type before adding
339    switch -exact -- $queues_sorting($name) {
340        0    {
341        foreach {item prio} $args {
342        if {![string is integer -strict $prio]} {
343            error "priority \"$prio\" is not an integer type value"
344        }
345        }
346    }
347        1    {
348        foreach {item prio} $args {
349        if {![string is double -strict $prio]} {
350            error "priority \"$prio\" is not a real type value"
351        }
352        }
353    }
354        default {
355        #no restrictions for -ascii and -dictionary
356    }
357    }
358
359    # sort by priorities
360    set opt [lindex $sortopt $queues_sorting($name)]
361    set dir [lindex $sortdir $queues_sorting($name)]
362
363    # add only if check has passed
364    foreach {item prio} $args {
365        set new [list $prio $item]
366        set queues($name) [__linsertsorted [K $queues($name) [set queues($name) ""]] $new $opt $dir]
367    }
368    return
369}
370
371# ::struct::prioqueue::_remove --
372#
373#   Delete an item together with it's related priority value from the queue.
374#
375# Arguments:
376#   name    name of the queue object
377#   item    item to be removed
378#
379# Results:
380#   None.
381
382if {[package vcompare [package present Tcl] 8.5] < 0} {
383    # 8.4-: We have -index option for lsearch, so we use glob to allow
384    # us to create a pattern which can ignore the priority value. We
385    # quote everything in the item to prevent it from being
386    # glob-matched, exact matching is required.
387
388    proc ::struct::prioqueue::_remove {name item} {
389	variable queues
390	set queuelist $queues($name)
391	set itemrep "* \\[join [split $item {}] "\\"]"
392	set foundat [lsearch -glob $queuelist $itemrep]
393
394	# the item to remove was not found if foundat remains at -1,
395	# nothing to replace then
396	if {$foundat < 0} return
397	set queues($name) [lreplace $queuelist $foundat $foundat]
398	return
399    }
400} else {
401    # 8.5+: We have the -index option, allowing us to exactly address
402    # the column used to search.
403
404    proc ::struct::prioqueue::_remove {name item} {
405	variable queues
406	set queuelist $queues($name)
407	set foundat [lsearch -index 1 -exact $queuelist $item]
408
409	# the item to remove was not found if foundat remains at -1,
410	# nothing to replace then
411	if {$foundat < 0} return
412	set queues($name) [lreplace $queuelist $foundat $foundat]
413	return
414    }
415}
416
417# ::struct::prioqueue::_size --
418#
419#   Return the number of objects on a queue.
420#
421# Arguments:
422#   name    name of the queue object.
423#
424# Results:
425#   count   number of items on the queue.
426
427proc ::struct::prioqueue::_size {name} {
428    variable queues
429    return [llength $queues($name)]
430}
431
432# ::struct::prioqueue::__linsertsorted
433#
434# Helper proc for inserting into a sorted list.
435#
436#
437
438proc ::struct::prioqueue::__linsertsorted {list newElement sortopt sortdir} {
439
440    set cmpcmd __elementcompare${sortopt}
441    set pos -1
442    set newPrio [lindex $newElement 0]
443
444    # do a binary search
445    set lower -1
446    set upper [llength $list]
447    set bound [expr {$upper+1}]
448    set pivot 0
449
450    if {$upper > 0} {
451        while {$lower +1 != $upper } {
452
453           # get the pivot element
454           set pivot [expr {($lower + $upper) / 2}]
455           set element [lindex $list $pivot]
456        set prio [lindex $element 0]
457
458           # check
459           set test [$cmpcmd $prio $newPrio $sortdir]
460           if {$test == 0} {
461                set pos $pivot
462                set upper $pivot
463                # now break as we need the last item
464                break
465           } elseif {$test > 0 } {
466                # search lower section
467                set upper $pivot
468                set bound $upper
469                set pos -1
470           } else {
471                # search upper section
472                set lower $pivot
473                set pos $bound
474           }
475        }
476
477
478        if {$pos == -1} {
479            # we do an insert before the pivot element
480            set pos $pivot
481        }
482
483        # loop to the last matching element to
484        # keep a stable insertion order
485        while {[$cmpcmd $prio $newPrio $sortdir]==0} {
486        incr pos
487            if {$pos > [llength $list]} {break}
488            set element [lindex $list $pos]
489            set prio [lindex $element 0]
490        }
491
492    } else {
493        set pos 0
494    }
495
496    # do the insert without copying
497    linsert [K $list [set list ""]] $pos $newElement
498}
499
500# ::struct::prioqueue::__elementcompare
501#
502# Compare helpers with the sort options.
503#
504#
505
506proc ::struct::prioqueue::__elementcompare-integer {prio newPrio sortdir} {
507    return [expr {$prio < $newPrio ? -1*$sortdir : ($prio != $newPrio)*$sortdir}]
508}
509
510proc ::struct::prioqueue::__elementcompare-real {prio newPrio sortdir} {
511    return [expr {$prio < $newPrio ? -1*$sortdir : ($prio != $newPrio)*$sortdir}]
512}
513
514proc ::struct::prioqueue::__elementcompare-ascii {prio newPrio sortdir} {
515    return [expr {[string compare $prio $newPrio]*$sortdir}]
516}
517
518proc ::struct::prioqueue::__elementcompare-dictionary {prio newPrio sortdir} {
519    # need to use lsort to access -dictionary sorting
520    set tlist [lsort -increasing -dictionary [list $prio $newPrio]]
521    set e1 [string equal [lindex $tlist 0]  $prio]
522    set e2 [string equal [lindex $tlist 1]  $prio]
523    return [expr {$e1 > $e2 ? -1*$sortdir : ($e1 != $e2)*$sortdir}]
524}
525
526# ### ### ### ######### ######### #########
527## Ready
528
529namespace eval ::struct {
530    # Get 'prioqueue::prioqueue' into the general structure namespace.
531    namespace import -force prioqueue::prioqueue
532    namespace export prioqueue
533}
534
535package provide struct::prioqueue 1.4
536