I need some help with writing a tcl code, to sort the data from a dictionary. The dictionary saves lists in a .txt file. I need to access the file and sort it through the second column of the lists.
1,0.8,bananas,,,,,
2,1.0,apples,,,,,
3,5.1,grapes,,,,,
4,2.4,oranges,,,,,
5,1.7,pineapples,,,,,
...
how can i sort that dict data, to look like that:
1,0.8,bananas,,,,,
2,1.0,apples,,,,,
5,1.7,pineapples,,,,,
4,2.4,oranges,,,,,
3,5.1,grapes,,,,,
...
please, can you help me making this sorting code?
i tried many codes, but with no sucess.
Tcl's lsort -command option would be useful here.
To use it, first define a proc that takes two arguments (usually called a and b) which returns -1, 0, or 1. Each pair of items in the list will be used as a pair of arguments to this proc.
set lines {
1,0.8,bananas,,,,,
2,1.0,apples,,,,,
3,5.1,grapes,,,,,
4,2.4,oranges,,,,,
5,1.7,pineapples,,,,,
}
proc sort_by_col2 {a b} {
set list_a [split $a ","]
set list_b [split $b ","]
set a2 [lindex $list_a 1]
set b2 [lindex $list_b 1]
if {$a2 < $b2} {
return -1
} elseif {$a2 > $b2} {
return 1
} else {
return 0
}
}
lsort -command sort_by_col2 $lines
--> 1,0.8,bananas,,,,,
2,1.0,apples,,,,,
5,1.7,pineapples,,,,,
4,2.4,oranges,,,,,
3,5.1,grapes,,,,,
My goal is to find the next key in an array... below my data :
# Index increment may change, there is not necessarily continuity like this example.
# My $index can be 1,2,3,4,8,12,25,32...
# but the size of my array is about 100,000 elements.
for {set index 1} {$index < 100000} {incr index} {
set refdata($index,Pt,X) [expr {10 + $index}]
}
I need to know the next key to be able to build a geometric line... I did not find in the help a command that allows me to find the next key of my array so I created my own function below :
proc SearchNextKeyArrayElement {dataarray mykey} {
upvar $dataarray myarray
set mydata [lsort -dictionary [array names myarray]]
set index [lsearch $mydata $mykey]
if {$index > -1} {
return [lindex $mydata [expr {$index + 1}]]
}
return ""
}
foreach k [lsort -dictionary [array names refdata]] {
if {[string match "*,Pt,*" $k]} {
set nextkey [SearchNextKeyArrayElement refdata $k]
}
}
And it takes a long time...array nextelement command is maybe the solution...But I do not understand how to use it ?
Here's an example:
start a search with array startsearch
loop while array anymore is true
get the next key with array nextelement
tidy up with array donesearch
use try {} catch {} finally for safety
# array foreach
# to be subsumed in Tcl 8.7 by `array for`
# https://core.tcl.tk/tips/doc/trunk/tip/421.md
#
# example:
# array set A {foo bar baz qux}
# array foreach {key val} A {puts "name=$key, value=$val"}
#
# A note on performance: we're not saving any time with this approach.
# This is essentially `foreach name [array names ary] {...}
# We are saving memory: iterating over the names versus extracting
# them all at the beginning.
#
proc array_foreach {vars arrayName body} {
if {[llength $vars] != 2} {
error {array foreach: "vars" must be a 2 element list}
}
lassign $vars keyVar valueVar
# Using the complicated `upvar 1 $arrayName $arrayName` so that any
# error messages propagate up with the user's array name
upvar 1 $arrayName $arrayName \
$keyVar key \
$valueVar value
set sid [array startsearch $arrayName]
# If the array is modified while a search is ongoing, the searchID will
# be invalidated: wrap the commands that use $sid in a try block.
try {
while {[array anymore $arrayName $sid]} {
set key [array nextelement $arrayName $sid]
set value [set "${arrayName}($key)"]
uplevel 1 $body
}
} trap {TCL LOOKUP ARRAYSEARCH} {"" e} {
puts stderr [list $e]
dict set e -errorinfo "detected attempt to add/delete array keys while iterating"
return -options $e
} finally {
array donesearch $arrayName $sid
}
return
}
Generally speaking, Tcl arrays have no order at all; they can change their order on any modification to the array or any of its elements. The commands that iterate over the array (array for, array get, array names, and the iteration commands array startsearch/array nextelement/array anymore) only work with the current order. However, you can use array names to get the element names into a Tcl list (which is order preserving), sort those to get the order that you're going to iterate over, and then use foreach over that. As long as you're not adding or removing elements, it'll be fine. (Adding elements is sort-of OK too; you'll just not see them in your iteration.)
foreach key [lsort -dictionary [array names myarray]] {
ProcessElement $key $myarray($key)
}
By contrast, trying to just go from one element to the next will hurt a lot; that operation is not exposed.
Using the iteration commands is done like this:
set s [array startsearch myarray]
while {[array anymore myarray $s]} {
set key [array nextelement myarray $s]
ProcessElement $key $myarray($key)
}
Note that you don't get an option to sort the search. You won't see these used much in production code; doing array names or array get is usually better. And now (well, 8.7 is still in alpha) you've also got array for:
array for {key value} myarray {
ProcessElement $key $value
}
Efficient for large arrays, but still doesn't permit sorting; supporting direct sorting would require a different sort of storage engine on the back of the array.
This is why it's slow: You're sorting the array names once for the foreach command and then again for each element. Sort once and cache it, then you can iterate over it much more efficiently
set sorted_names [lsort -dictionary [array names refdata -glob {*,Pt,*}]]
set len [llength $sorted_names]
for {set i 0; set j 1} {$i < $len} {incr i; incr j} {
set this_name [lindex $sorted_names $i]
set next_name [lindex $sorted_names $j]
# ...
}
I have two questions:
In one of my tcl functions an array is declared and initialized as follows:
foreach port $StcPorts {
set dutport [lindex $DutPortsTmp $i]
set STCPort($dutport) [list 0 [lindex [lindex $cardlist $port] 0] [lindex [lindex $cardlist $port] 1]]
}
This is pretty confusing to me since I am new to tcl. Please help me to understand the above code on STCPort creation.
Question 2:
In the same file, STCPort is being used like this:
foreach dutport $DutPortsTmp {
set slot [lindex $STCPort($dutport) 1]
set port [lindex $STCPort($dutport) 2]
set hPort [stc::create port -under $hProject -location //$stcipaddress/$slot/$port -useDefaultHost False ]
lappend STCPort($dutport) $hPort
}
I have sourced this file and my requirement is to get the hPort value in another function residing in another file and work on it. Below is the function d_stc:
proc d_stc {args} {
global DutPorts STCPort
upvar 1 $STCPort _STCPort
#I am trying to get hPort by array index below:
foreach DutPort $DutPorts {
set card [lindex $_STCPort($DutPort) 1]
set port [lindex $_STCport($DutPort) 2]
set hport [lindex $_STCPort($DutPort) 3]
}
And, I am getting the following error:
can't read "STCPort": variable is array
while executing
"upvar 1 $STCPort _STCPort"
(procedure "d_stc" line 3)
I have used global to access the array in this function. But, where I am going wrong?
Thanks,
foreach port $StcPorts {
set dutport [lindex $DutPortsTmp $i]
set STCPort($dutport) [list 0 [lindex [lindex $cardlist $port] 0] [lindex [lindex $cardlist $port] 1]]
}
This snippet goes through the elements of the list StcPorts, which seem to be integers representing some kind of port numbers. For every port number, an array index (stored as dutport) is generated by taking the ith element from the list DutPortsTmp. The value of i is unexplained by this snippet. Also for everly port number, an array value in the array STCPort is created, with the index created above, and a value constructed like this:
Get the element in the list cardlist that corresponds to the integer index port (call that C)
Form a list from the three items 0, the first subitem in C, and the second subitem in C.
.
foreach dutport $DutPortsTmp {
set slot [lindex $STCPort($dutport) 1]
set port [lindex $STCPort($dutport) 2]
set hPort [stc::create port -under $hProject -location //$stcipaddress/$slot/$port -useDefaultHost False ]
lappend STCPort($dutport) $hPort
}
The list DutPortsTmp, which was used to create array indexes, reappears. We work with every item in the list, with the list item/array index again being named dutport. For every iteration, we look at the corresponding array item, setting slot to the second item of the array item's value (the first item in C, above) and port to the third item of the array item's value (the second item in C). The result of an invocation of stc::create port is assigned to the variable hPort, and the value of the array item is extended with a new list item, the value in hPort.
proc d_stc {args} {
global DutPorts STCPort
upvar 1 $STCPort _STCPort
#I am trying to get hPort by array index below:
foreach DutPort $DutPorts {
set card [lindex $_STCPort($DutPort) 1]
set port [lindex $_STCport($DutPort) 2]
set hport [lindex $_STCPort($DutPort) 3]
}
This should probably be:
proc d_stc {args} {
global DutPorts STCPort
foreach DutPort $DutPorts {
set hport [lindex $STCPort($DutPort) 3]
}
}
The corrected snippet again goes through the array indexes for STCPort, but this time DutPorts is used instead of DutPortsTmp to get the array indexes. The hport variable gets the value of the fourth item in each of the array items.
But: the value in hport is never used or returned anywhere, and the value is overwritten by a new value from each new item looked at.
upvar 1 $STCPort _STCPort
does not work since there is no string value in STCPort. If the procedure is called from the global level, this would work:
upvar 1 STCPort _STCPort
(See the difference? Not the value of STCPort, but the string "STCPort" itself.) You don't need this: the invocation of global DutPorts STCPort already makes the array STCPort visible inside the procedure.
I have a file like this:
set position {0.50 0.50}
set visibility false
set text {ID: {entity.id}\n Value: {entity.contour_val}}
And I want to do something similar to source, but I want to use a file handle only.
My current attempt looks like this:
proc readArray {fileHandle arrayName} {
upvar $arrayName arr
set cl 0
while {! [eof $fileHandle]} {
set cl [expr "$cl + 1"]
set line [gets $fileHandle]
if [$line eq {}] continue
puts $line
namespace eval ::__esg_priv "
uplevel 1 {*}$line
"
info vars ::__esg_priv::*
foreach varPath [info vars ::__esg_priv::*] {
set varName [string map { ::__esg_priv:: "" } $varPath]
puts "Setting arr($varName) -> [set $varPath]"
set arr($varName) [set $varPath]
}
namespace delete __esg_priv
}
puts "$cl number of lines read"
}
In place of uplevel I tried many combinations of eval and quoting.
My problem is, it either fails on the lines with lists or it does not actuall set the variables.
What is the right way to do it, if the executed commands are expected to be any valid code.
An extra question would be how to properly apply error checking, which I haven't tried yet.
After a call to
readArray [open "myFile.tcl" r] arr
I expect that
parray arr
issues something like:
arr(position) = 0.50 0.50
arr(text) = ID: {entity.id}\n Value: {entity.contour_val}
arr(visibility) = false
BTW: The last line contains internal {}, which are supposed to make it into the string variables. And there is no intent to make this a dict.
This code works, but there are still some problems with it:
proc readArray {fileHandle arrayName} {
upvar $arrayName arr
set cl 0
while {! [eof $fileHandle]} {
incr cl ;# !
set line [gets $fileHandle]
if {$line eq {}} continue ;# !
puts $line
namespace eval ::__esg_priv $line ;# !
foreach varPath [info vars ::__esg_priv::*] {
set varName [string map { ::__esg_priv:: "" } $varPath]
puts "Setting arr($varName) -> [set $varPath]"
set arr($varName) [set $varPath]
}
namespace delete __esg_priv
}
puts "$cl number of lines read"
}
I've taken out a couple of lines that didn't seem necessary, and changed some lines a bit.
You don't need set cl [expr "$cl + 1"]: incr cl will do.
if [$line eq {}] continue will fail because the [...] is a command substitution. if {$line eq {}} continue (braces instead of brackets) does what you intend.
Unless you are accessing variables in another scope, you won't need uplevel. namespace eval ::__esg_priv $line will evaluate one line in the designated namespace.
I didn't change the following, but maybe you should:
set varName [string map { ::__esg_priv:: "" } $varPath] works as intended, but set varName [namespace tail $varPath] is cleaner.
Be aware that if there exists a global variable with the same name as one of the variables in your file, no namespace variable will be created; the global variable will be updated instead.
If you intend to use the value in the text variable as a dictionary, you need to remove either the \n or the braces.
According to your question title, you want to evaluate the file line by line. If that requirement can be lifted, your code could be simplified by reading the whole script in one operation and then evaluating it with a single namespace eval.
ETA
This solution is a lot more robust in that it reads the script in a sandbox (always a good idea when writing code that will execute arbitrary external code) and redefines (within that sandbox) the set command to create members in your array instead of regular variables.
proc readArray {fileHandle arrayName} {
upvar 1 $arrayName arr
set int [interp create -safe]
$int alias set apply {{name value} {
uplevel 1 [list set arr($name) $value]
}}
$int eval [read $fileHandle]
interp delete $int
}
To make it even more safe against unexpected interaction with global variables etc, look at the interp package in the Tcllib. It lets you create an interpreter that is completely empty.
Documentation: apply, continue, eof, foreach, gets, if, incr, info, interp package, interp, list, namespace, proc, puts, set, string, uplevel, upvar, while
I want to create a list and each element of it is an array, similarly to an array of structs in C language.
Can it be done in TCL and how if it can? thanks very much!
I did some try but it failed...
tcl>set si(eid) -1
tcl>set si(core) 0
tcl>set si(time) 0
tcl>lappend si_list "$si"
Error: can't read "si": variable is array
You can't create a list of arrays, but you can create a list of dicts which is functionally the same thing (a mapping from keys to values):
set mylist [list [dict create a 1 b 2] [dict create a 4 b 5]]
puts [dict get [lindex $mylist 1] a]
To do it as arrays you need to use [array get] and [array set] to change the array into a string:
set si(eid) -1
set si(core) 0
set si(time) 0
lappend si_list [array get si]
And to get it back out
array set newsi [lindex $si_list]
puts $newsi(eid)
dicts let you work on the {name value} lists directly.
One way to do this on versions of Tcl that don't include dict is to use upvar.
To do this, add the names of the array variables to your list:
set si(eid) -1
set si(core) 0
set si(time) 0
lappend si_list "si"
Then to get your array back, do this:
upvar #0 [lindex $si_list 0] newsi
puts $newsi(eid)
You could also use the ::struct::record package from tcllib for something like that.