struct::list(3tcl)
๐ struct::list โ the NAME bit ๐
struct::list โ procedures for manipulating lists, and yes bestie that is the whole vibe. โจ
This one lives in the Tcl Data Structures manual, section 3tcl, and it's genuinely one of the most useful namespaces in the whole Tcl ecosystem. iykyk ๐ซถ
๐ธ SYNOPSIS ๐ธ
Okay so first you gotta get the package in the door:
package require Tcl 8.5 9
package require struct::list ?1.9?
Then literally everything else is a subcommand of ::struct::list:
::struct::list longestCommonSubsequence sequence1 sequence2 ?maxOccurs?
::struct::list longestCommonSubsequence2 sequence1 sequence2 ?maxOccurs?
::struct::list lcsInvert lcsData len1 len2
::struct::list lcsInvert2 lcs1 lcs2 len1 len2
::struct::list lcsInvertMerge lcsData len1 len2
::struct::list lcsInvertMerge2 lcs1 lcs2 len1 len2
::struct::list reverse sequence
::struct::list shuffle list
::struct::list assign sequence varname ?varname?...
::struct::list flatten ?-full? ?--? sequence
::struct::list map sequence cmdprefix
::struct::list mapfor var sequence script
::struct::list filter sequence cmdprefix
::struct::list filterfor var sequence expr
::struct::list split sequence cmdprefix ?passVar failVar?
::struct::list fold sequence initialvalue cmdprefix
::struct::list shift listvar
::struct::list iota n
::struct::list equal ?-simple? ?--? a b
::struct::list repeat size element1 ?element2 element3...?
::struct::list repeatn value size...
::struct::list dbJoin ?-inner|-left|-right|-full? ?-keys varname? {keycol table}...
::struct::list dbJoinKeyed ?-inner|-left|-right|-full? ?-keys varname? table...
::struct::list swap listvar i j
::struct::list firstperm list
::struct::list nextperm perm
::struct::list permutations list
::struct::list foreachperm var list body
โจ DESCRIPTION โจ
The ::struct::list namespace is a whole treasure chest of commands for processing Tcl lists. Generally speaking (fr fr), these implement algorithms that are more complex or more specialized than the ones Tcl itself hands you out of the box. So when the built-ins aren't enough, this is your girl. ๐
It exports only a single command, struct::list. Every single bit of functionality in here is reachable through a subcommand of that one command. One command to rule them all, no cap. ๐
๐ COMMANDS ๐
๐ฆ ::struct::list longestCommonSubsequence sequence1 sequence2 ?maxOccurs?
Returns a list of indices into sequence1 plus a corresponding list where each item is an index into sequence2 of the matching value, according to the longest common subsequences algorithm. If you pass maxOccurs, the common subsequence gets restricted to elements that occur no more than maxOccurs times in sequence2. Lowkey a vibe check on how often stuff repeats.
๐ฆ ::struct::list longestCommonSubsequence2 sequence1 sequence2 ?maxOccurs?
Returns the longest common subsequence of elements in the two lists sequence1 and sequence2. If maxOccurs is provided, the result is only an approximation โ it's approximated by first determining the longest common sequence of only those elements that occur no more than maxOccurs times in sequence2, and then using that result to align the two lists, determining the longest common subsequences of the sublists between the two elements.
The result is the same as for longestCommonSubsequence.
๐ฆ ::struct::list lcsInvert lcsData len1 len2
Takes a description of a longest common subsequence (lcsData), inverts it, and returns the result. Inversion here means: the input describes which parts of the two sequences are identical, so the output describes the differences instead. Glow-down, basically.
To be fully defined, the lengths of the two sequences have to be known, and you pass them in through len1 and len2. No guessing allowed, bestie. ๐
The result is a list where each element describes one chunk of the differences between the two sequences. This description is a list containing three elements: a type, and two pairs of indices into sequence1 and sequence2 respectively, in this order. The type can be one of three values:
addedโ describes an addition. I.e. items which are missing insequence1can be found insequence2. The pair of indices intosequence1describes where the added range had been expected to be insequence1. The first index refers to the item just before the added range, and the second index refers to the item just after the added range. The pair of indices intosequence2describes the range of items which has been added to it. The first index refers to the first item in the range, and the second index refers to the last item in the range.deletedโ describes a deletion. I.e. items which are insequence1are missing fromsequence2. The pair of indices intosequence1describes the range of items which has been deleted. The first index refers to the first item in the range, and the second index refers to the last item in the range. The pair of indices intosequence2describes where the deleted range had been expected to be insequence2. The first index refers to the item just before the deleted range, and the second index refers to the item just after the deleted range.changedโ describes a general change. I.e. a range of items insequence1has been replaced by a different range of items insequence2. The pair of indices intosequence1describes the range of items which has been replaced. The first index refers to the first item in the range, and the second index refers to the last item in the range. The pair of indices intosequence2describes the range of items replacing the original range. Again the first index refers to the first item in the range, and the second index refers to the last item in the range.
Okay here's the moment, the visual, the aesthetic ๐:
sequence 1 = {a b r a c a d a b r a}
lcs 1 = {1 2 4 5 8 9 10}
lcs 2 = {0 1 3 4 5 6 7}
sequence 2 = {b r i c a b r a c}
Inversion = {{deleted {0 0} {-1 0}}
{changed {3 3} {2 2}}
{deleted {6 7} {4 5}}
{added {10 11} {8 8}}}
Notes, and these matter so don't skip them ๐:
- An index of
-1in adeletedchunk refers to just before the first element of the second sequence. - Also an index equal to the length of the first sequence in an
addedchunk refers to just behind the end of the sequence.
๐ฆ ::struct::list lcsInvert2 lcs1 lcs2 len1 len2
Similar to lcsInvert. Instead of directly taking the result of a call to longestCommonSubsequence, this subcommand expects the indices for the two sequences in two separate lists. Same energy, different packaging. โจ
๐ฆ ::struct::list lcsInvertMerge lcsData len1 len2
Similar to lcsInvert. It returns essentially the same structure as that command, except that it may contain chunks of type unchanged too. Yes bestie, a whole extra type, we love an upgrade. ๐ซถ
These new chunks describe the parts which are unchanged between the two sequences. This means that the result of this command describes both the changed and unchanged parts of the two sequences in one structure. One structure, everything, periodt.
sequence 1 = {a b r a c a d a b r a}
lcs 1 = {1 2 4 5 8 9 10}
lcs 2 = {0 1 3 4 5 6 7}
sequence 2 = {b r i c a b r a c}
Inversion/Merge = {{deleted {0 0} {-1 0}}
{unchanged {1 2} {0 1}}
{changed {3 3} {2 2}}
{unchanged {4 5} {3 4}}
{deleted {6 7} {4 5}}
{unchanged {8 10} {5 7}}
{added {10 11} {8 8}}}
๐ฆ ::struct::list lcsInvertMerge2 lcs1 lcs2 len1 len2
Similar to lcsInvertMerge. Instead of directly taking the result of a call to longestCommonSubsequence, this subcommand expects the indices for the two sequences in two separate lists. Yeah, it's the same twist as lcsInvert2. Consistency, iconic. ๐
๐ฆ ::struct::list reverse sequence
Takes a single sequence as argument and returns a new sequence containing the elements of the input sequence in reverse order. Flip it and reverse it. ๐ฅ
๐ฆ ::struct::list shuffle list
Takes a list and returns a copy of that list with the elements it contains in random order. Every possible ordering of elements is equally likely to be generated. The Fisher-Yates shuffling algorithm is used internally. So it's fair, it's random, it's not rigged, no cap. ๐ฒ
๐ฆ ::struct::list assign sequence varname ?varname?...
Assigns the first n elements of the input sequence to the one or more variables whose names were listed after the sequence, where n is the number of specified variables. Destructuring assignment, but make it Tcl. ๐
If there are more variables specified than there are elements in the sequence, the empty string will be assigned to the superfluous variables. No errors, no drama, just "".
If there are more elements in the sequence than variable names specified, the subcommand returns a list containing the unassigned elements. Else an empty list is returned. Leftovers stay on the plate ๐.
tclsh> ::struct::list assign {a b c d e} foo bar
c d e
tclsh> set foo
a
tclsh> set bar
b
๐ฆ ::struct::list flatten ?-full? ?--? sequence
Takes a single sequence and returns a new sequence where one level of nesting was removed from the input sequence. In other words, the sublists in the input sequence are replaced by their elements. It's giving "un-nest me". ๐ฆ
The subcommand will remove any nesting it finds if the option -full is specified. Full send, full flatten.
tclsh> ::struct::list flatten {1 2 3 {4 5} {6 7} {{8 9}} 10}
1 2 3 4 5 6 7 {8 9} 10
tclsh> ::struct::list flatten -full {1 2 3 {4 5} {6 7} {{8 9}} 10}
1 2 3 4 5 6 7 8 9 10
๐ฆ ::struct::list map sequence cmdprefix
Takes a sequence to operate on and a command prefix (cmdprefix) specifying an operation, applies the command prefix to each element of the sequence, and returns a sequence consisting of the results of that application. It's the classic map, and it ate. ๐
The command prefix will be evaluated with a single word appended to it. The evaluation takes place in the context of the caller of the subcommand. Important detail, don't forget it bestie.
tclsh> # squaring all elements in a list
tclsh> proc sqr {x} {expr {$x*$x}}
tclsh> ::struct::list map {1 2 3 4 5} sqr
1 4 9 16 25
tclsh> # Retrieving the second column from a matrix
tclsh> # given as list of lists.
tclsh> proc projection {n list} {::lindex $list $n}
tclsh> ::struct::list map {{a b c} {1 2 3} {d f g}} {projection 1}
b 2 f
๐ฆ ::struct::list mapfor var sequence script ๐ฆ
okay so bestie, mapfor is the glow-up sibling of map. Same energy, different delivery. You hand it a sequence to work on and a Tcl script, it runs that script on every single element, and hands you back a new sequence made of all the results. โจ
The script gets evaluated as is, and it can peek at the element it's currently chewing on through the iteration variable var you named. And where does all this evaluating happen? In the context of the caller of the subcommand. iykyk ๐
tclsh> # squaring all elements in a list
tclsh> ::struct::list mapfor x {1 2 3 4 5} {
expr {$x * $x}
}
1 4 9 16 25
tclsh> # Retrieving the second column from a matrix
tclsh> # given as list of lists.
tclsh> ::struct::list mapfor x {{a b c} {1 2 3} {d f g}} {
lindex $x 1
}
b 2 f
๐ธ ::struct::list filter sequence cmdprefix ๐ธ
The vibe check command, fr fr. You give it a sequence to operate on and a command prefix (cmdprefix) describing an operation. It applies that command prefix to each element, and returns a sequence of only the elements that passed the test โ i.e. the ones where the command prefix returned true. Everything that failed justโฆ doesn't make the cut. ๐ซถ
The command prefix gets evaluated with a single word appended to it, and that evaluation takes place in the context of the caller of the subcommand.
tclsh> # removing all odd numbers from the input
tclsh> proc even {x} {expr {($x % 2) == 0}}
tclsh> ::struct::list filter {1 2 3 4 5} even
2 4
Note: the filter is a specialized application of fold where the result is extended with the current item or not, depending on the result of the test. ๐
๐ง ::struct::list filterfor var sequence expr ๐ง
Same as filter but the test is written inline as a Tcl expr, no helper proc required, no extra steps, main character energy. You give it a sequence to operate on and an expr condition. It applies that condition to each element and returns the sequence of everything that came back true. Everyone who failed the test gets filtered out, sorry not sorry. ๐
The expression is evaluated as is, and it can reach the current list element through the iteration variable var. The evaluation happens in the context of the caller of the subcommand.
tclsh> # removing all odd numbers from the input
tclsh> ::struct::list filterfor x {1 2 3 4 5} {($x % 2) == 0}
2 4
๐ ::struct::list split sequence cmdprefix ?passVar failVar? ๐
This one is a variant of filter (scroll up bestie). But instead of only getting the passing elements back, you get both the passing list and the failing list. Obsessed with the transparency honestly. ๐
- If no variable names are specified, the result is a list containing the list of passing elements and the list of failing elements โ in that order.
- Otherwise, the passing and failing lists get stored into the two specified variables, and the result is a list containing two numbers: how many elements passed the test, and how many failed โ again, in that order.
The interface to the test is the exact same one filter uses. No new rules, no cap.
โญ ::struct::list fold sequence initialvalue cmdprefix โญ
The subcommand takes a sequence to operate on, an arbitrary string initialvalue, and a command prefix (cmdprefix) specifying an operation.
The command prefix gets evaluated with two words appended to it. The second of those two words will always be an element of the sequence. And yes, the evaluation takes place in the context of the caller of the subcommand. ๐ซถ
Then it squishes the whole sequence down into one single value by repeatedly applying that command prefix, and returns that value. The reduction goes like this:
- Apply the command to the initial value and the first element of the list.
- Apply the command to the result of the last call and the second element of the list.
...
i. Apply the command to the result of the last call and the i'th element of the list.
...
end. Apply the command to the result of the last call and the last element of the list. The result of this call is what gets returned as the result of the subcommand. periodt. ๐
tclsh> # summing the elements in a list.
tclsh> proc + {a b} {expr {$a + $b}}
tclsh> ::struct::list fold {1 2 3 4 5} 0 +
15
๐ ::struct::list shift listvar ๐
Takes the list sitting inside the variable named by listvar and shifts it down by one element. After the call, listvar will hold a list containing the second-to-last elements of the input list, and the first element of that list is returned as the result of the command.
Shifting the empty list does nothing. It just vibes. ๐
๐ ::struct::list iota n ๐
Returns a list containing the integer numbers in the range [0,n). The element at index i of the list contains the number i. Clean, simple, iconic.
For n == 0 an empty list will be returned.
๐ ::struct::list equal ?-simple? ?--? a b ๐
Compares the two lists a and b for equality. And by equality I mean: same length, same elements, same order. No substitutions, no rearranging. โจ
-simpleโ by default, equality checks are applied recursively to list elements which are lists themselves. This behaviour is disabled when you specify-simple.--โ disables option processing even if the following argument looks like such an option. The classic escape hatch, love her.
The result of the command is a boolean value. True if the two lists are equal, false otherwise. Slay or don't slay, no in-between. ๐
๐ฆ ::struct::list repeat size element1 ?element2 element3...? ๐ฆ
Creates a list of length size * number of elements by repeating size times the sequence of elements element1 element2 .... size must be a positive integer, and elementn can be any Tcl value. ๐ซถ
Note thatrepeat 1 arg ...is identical tolist arg ..., though the arg is required withrepeat. No skipping it, bestie.
Examples:
tclsh> ::struct::list repeat 3 a
a a a
tclsh> ::struct::list repeat 3 [::struct::list repeat 3 0]
{0 0 0} {0 0 0} {0 0 0}
tclsh> ::struct::list repeat 3 a b c
a b c a b c a b c
tclsh> ::struct::list repeat 3 [::struct::list repeat 2 a] b c
{a a} b c {a a} b c {a a} b c
๐ ::struct::list repeatn value size... ๐
Creates a (nested) list containing value in all positions. The exact size and degree of nesting is decided by the size arguments โ and every single one of them has to be an integer number greater than or equal to zero. ๐
A single argument size which is a list of more than one element will be treated as if more than one size argument was specified. Sneaky, but valid.
- If only one size argument is present, the returned list will not be nested: it'll be of length size and contain value in all positions.
- If more than one size argument is present, the returned list will be nested, and of the length specified by the last size argument given to it. The elements of that list are defined as the result of
Repeatfor the same arguments, but with the last size value removed.
An empty list will be returned if no size arguments are present.
tclsh> ::struct::list repeatn 0 3 4
{0 0 0} {0 0 0} {0 0 0} {0 0 0}
tclsh> ::struct::list repeatn 0 {3 4}
{0 0 0} {0 0 0} {0 0 0} {0 0 0}
tclsh> ::struct::list repeatn 0 {3 4 5}
{{0 0 0} {0 0 0} {0 0 0} {0 0 0}} {{0 0 0} {0 0 0} {0 0 0} {0 0 0}} {{0 0 0} {0 0 0} {0 0 0} {0 0 0}} {{0 0 0} {0 0 0} {0 0 0} {0 0 0}} {{0 0 0} {0 0 0} {0 0 0} {0 0 0}}
๐ ::struct::list dbJoin ?-inner|-left|-right|-full? ?-keys varname? {keycol table}... ๐
The method performs a table join according to relational algebra. Yes bestie, we're doing databases now. ๐
-innerโ performs a regular inner join.-leftโ triggers the left outer join operation.-rightโ triggers the right outer join operation.-fullโ triggers the full outer join operation.-keysvarname โ its argument is the name of a variable to store the full list of found keys into.--โ standard end-of-options marker.
The execution of any of the possible outer join operations is triggered by the presence of either -left, -right, or -full. If none of those options is present, a regular inner join will be performed. That can also be triggered explicitly by specifying -inner. The various possible join operations are explained in detail in the TABLE JOIN section. ๐ฅ
If the -keys is present, its argument is the name of a variable to store the full list of found keys into. Depending on the exact nature of the input table and the join mode, the output table may not contain all the keys by default. In such a case the caller can declare a variable for this information and then insert it into the output table on its own, as she will have more information about the placement than this command. main character behaviour, honestly. ๐ซถ
What's left to explain is the format of the arguments.
The keycol arguments are the indices of the columns in the tables which contain the key values to use for the joining. Each argument applies to the table following immediately after it. The columns are counted from 0, which references the first column. The table associated with the column index has to have at least keycol+1 columns.
An error will be thrown if there are less. ๐ญ
The table arguments are giving "here's a whole matrix, bestie" โ a table or matrix of rows and columns of values. We use the exact same representation as the one generated and consumed by the methods get rect and set rect of matrix objects. So, in other words: each argument is a list, and that list represents the whole matrix. Its elements are lists too, each one representing a single row of the matrix. And the elements of those row-lists? Those are the column values. ๐ซถ
The table that comes out of the join operation is returned as the result of the command, and it uses the same representation described above for the input tables. Symmetry, we love to see it. โจ
::struct::list dbJoinKeyed ?-inner|-left|-right|-full? ?-keys varname? table...
Okay so this one does literally the same operations as dbJoin, no cap. The only difference is how you spec the keys. Instead of passing column indices separately from the table, the keys are provided inside the table itself. So the row elements in each table aren't plain lists of column values anymore โ they're a two-element list where the second element is the regular list of column values and the first element is the key to use. Key first, values second. That's the whole vibe. ๐
::struct::list swap listvar i j
This subcommand exchanges the elements at the indices i and j in the list stored in the variable named by listvar. It's modified in place, and it's also returned as the result of the subcommand. So you get the glow-up and the receipt. ๐
::struct::list firstperm list
Returns the lexicographically first permutation of the input list. The origin story of the permutation journey. ๐ธ
::struct::list nextperm perm
This subcommand accepts a permutation of a set of elements (that's perm, bestie) and returns the next permutation in lexicographic sequence. The algorithm used here is by Donal E. Knuth โ see the REFERENCES section for the details, iykyk. ๐
::struct::list permutations list
Returns a list containing all permutations of the input list, in lexicographic order. Every single one. The whole lineup. โญ
::struct::list foreachperm var list body
This subcommand executes the script body once for each permutation of the specified list. The permutations are visited in lexicographic order, and the variable var is set to the permutation for which body is currently being executed. The result of the loop command is the empty string. That's it, that's the result. ๐ฆ
๐ LONGEST COMMON SUBSEQUENCE AND FILE COMPARISON ๐
The longestCommonSubsequence subcommand is the core of a flexible system for doing differential comparisons of files, similar to what the Unix command diff can do. And like โ for lots of file comparison tasks this procedure is genuinely rapid, fr fr. BUT. Its performance degrades severely if sequence2 contains many equal elements โ for instance, if you use this procedure to compare two files and a quarter of whose lines are blank. ๐ญ This drawback is intrinsic to the algorithm used (see the REFERENCES section for details). It's not a skill issue, it's a math issue.
One approach to dealing with the performance problem that is sometimes effective in practice is arbitrarily excluding elements that appear more than a certain number of times. That number is provided as the maxOccurs parameter. If frequent lines get excluded this way, they won't appear in the common subsequence that's computed โ what you get instead is the longest common subsequence of infrequent elements. The procedure longestCommonSubsequence2 implements this heuristic. It works as a wrapper around longestCommonSubsequence: it computes the longest common subsequence of infrequent elements, and then subdivides the subsequences that lie between the matches to approximate the true longest common subsequence. Close enough energy. ๐
๐งฎ TABLE JOIN ๐งฎ
This is an operation from relational algebra for relational databases. Very "I took a databases class" of it.
The easiest way to understand the regular inner join is that it creates the cartesian product of all the tables involved first, and then keeps only those rows in the resulting table for which the values in the specified key columns are equal to each other.
Now โ implementing that description naively, i.e. exactly as described above, will generate a huge intermediate result. ๐ To avoid this, the cartesian product and the filtering of rows are done at the same time. What's required is a fast way to determine if a key is present in a table. In a true database this is done through indices; here we use arrays internally. Sneaky, but iconic.
An outer join is an extension of the inner join for two tables. There are three variants of outer joins, called left, right, and full outer joins. Their result always contains all rows from an inner join, and then some additional rows. Extra rows, as a treat. โจ
- For the left outer join the additional rows are all rows from the left table for which there is no key in the right table. They are joined to an empty row of the right table to fit them into the result.
- For the right outer join the additional rows are all rows from the right table for which there is no key in the left table. They are joined to an empty row of the left table to fit them into the result.
- The full outer join combines both left and right outer join. In other words, the additional rows are as defined for left outer join, and right outer join, combined.
We extend all the joins from two to n tables (n > 2) by executing
(...((table1 join table2) join table3) ...) join tableN
Examples for all the joins, bestie, take notes ๐:
Inner Join
{0 foo} {0 bagel} {0 foo 0 bagel}
{1 snarf} inner join {1 snatz} = {1 snarf 1 snatz}
{2 blue} {3 driver}
Left Outer Join
{0 foo} {0 bagel} {0 foo 0 bagel}
{1 snarf} left outer join {1 snatz} = {1 snarf 1 snatz}
{2 blue} {3 driver} {2 blue {} {}}
Right Outer Join
{0 foo} {0 bagel} {0 foo 0 bagel}
{1 snarf} right outer join {1 snatz} = {1 snarf 1 snatz}
{2 blue} {3 driver} {{} {} 3 driver}
Full Outer Join
{0 foo} {0 bagel} {0 foo 0 bagel}
{1 snarf} full outer join {1 snatz} = {1 snarf 1 snatz}
{2 blue} {3 driver} {2 blue {} {}}
{{} {} 3 driver}
๐ REFERENCES ๐
- J. W. Hunt and M. D. McIlroy, "An algorithm for differential file comparison," Comp. Sci. Tech. Rep. #41, Bell Telephone Laboratories (1976). Available on the Web at the second author's personal site:
http://www.cs.dartmouth.edu/~doug/ - Donald E. Knuth, "Fascicle 2b of 'The Art of Computer Programming' volume 4". Available on the Web at the author's personal site:
http://www-cs-faculty.stanford.edu/~knuth/fasc2b.ps.gz.
๐ BUGS, IDEAS, FEEDBACK ๐
This document, and the package it describes, will undoubtedly contain bugs and other problems. No cap. Please report such in the category struct :: list of the Tcllib Trackers at http://core.tcl.tk/tcllib/reportlist. Please also report any ideas for enhancements you may have for either the package and/or the documentation. We want the feedback, genuinely. ๐ซถ
When proposing code changes, please provide unified diffs, i.e. the output of diff -u.
Note further that attachments are strongly preferred over inlined patches. Attachments can be made by going to the Edit form of the ticket immediately after its creation, and then using the left-most button in the secondary navigation bar. Go make the attachment, bestie, it's right there. ๐
๐ KEYWORDS ๐
Fisher-Yates, assign, common, comparison, diff, differential, equal, equality, filter, first permutation, flatten, folding, full outer join, generate permutations, inner join, join, left outer join, list, longest common subsequence, map, next permutation, outer join, permutation, reduce, repeating, repetition, reshuffle, reverse, right outer join, shuffle, subsequence, swapping
๐๏ธ CATEGORY ๐๏ธ
Data structures
ยฉ๏ธ COPYRIGHT ยฉ๏ธ
Copyright (c) 2003-2005 by Kevin B. Kenny. All rights reserved Copyright (c) 2003-2012 Andreas Kupries <andreas_kupries@users.sourceforge.net>
and that's a wrap on struct::list, bestie!! this whole glow-up came to you courtesy of tcllib, version 1.9, and it's the struct::list(3tcl) page โ read it, love it, go shuffle some lists. periodt. ๐โจ๐๐ฆ๐ธ๐
๐ซถ
๐ฆ where this page comes from
- debian trixie: tcllib 2.0+dfsg-4 ยท Tcl
- ubuntu questing: tcllib 2.0+dfsg-4
- ubuntu resolute: tcllib 2.0+dfsg-5 ยท Tcl
- ubuntu stonking: tcllib 2.0+dfsg-6 ยท Tcl
- Copyright (c) 2003-2005 by Kevin B\&. Kenny\&. All rights reserved
- Copyright (c) 2003-2012 Andreas Kupries <andreas_kupries@users\&.sourceforge\&.net>
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms