*! version 1.2.4 20261103 github.com/mdroste/stata-ctools *! _ctools_group — group identifiers from the ctools plugin, in place of egen group() *! *! Usage: _ctools_group varlist [if] [in], generate(newvar) [label] *! _ctools_group varlist [if] [in], count [strict] *! strict returns plugin errors instead of sorting in Stata. *! *! Numbers the distinct values of varlist 1, 2, ... in sort order, as *! egen group() does: observations excluded by if/in, or with a missing value *! in any variable of varlist (string "" counts as missing), get missing. The *! new variable is a long labeled "group(varlist)". label also defines a value *! label named after the new variable, with egen group(, label)'s text for *! each group. count only counts the groups. Returns r(N_groups), and with *! label r(reps), one observation number of each group in group order. *! *! The plugin reads, sorts and numbers the keys. Stata numbers them by sorting *! only when the plugin is unavailable or cannot read a key (strL text over *! 2,045 bytes). program define _ctools_group, rclass version 14.1 syntax varlist [if] [in], [GENerate(name) LABel COUNT STRICT] if ("`generate'" == "") == ("`count'" == "") | ("`label'" != "" & "`generate'" == "") { di as error "_ctools_group: specify generate() [label] or count" exit 198 } if "`generate'" != "" { confirm new variable `generate' if "`label'" != "" { capture label list `generate' if !_rc { di as error "default label `generate' already defined" exit 110 } } } local nkeys : word count `varlist' local rc = 199 capture _ctools_load if !_rc { capture program ctools_plugin, plugin using("`__ctools_plugin'") if _rc == 0 | _rc == 110 local rc = 0 } capture confirm number 0 if !`rc' { _ctools_strw `varlist' local out "" if "`generate'" != "" { * The plugin writes every observation of the range, so without in * the variable is created without initial values. if `"`in'"' == "" mata: (void) st_addvar("long", st_local("generate"), 1) else quietly generate long `generate' = . local out "out" } local reps = cond("`label'" != "", "reps", "") * Stata answers an if one observation at a time. Without one the * plugin skips the check; an if that names a numeric variable (the * callers' touse) passes that variable last, as a sample marker the * plugin reads in parallel. Any other if is marked first, unless in * limits the range (marking covers every observation); that marker is * the last variable until it is dropped, which is then free. local select "skipif" local marker "" local marked "" local ifcond "" if `"`if'"' != "" { local select "touse" local marker = strtrim(substr(`"`if'"', 3, .)) capture confirm numeric variable `marker', exact if _rc | wordcount(`"`marker'"') != 1 { local marker "" if `"`in'"' == "" { tempvar marker capture mark `marker' `if' local rc = _rc local marked `marker' } else { local select "" local ifcond `"`if'"' } } } local ctools_group_reps "" if !`rc' { capture plugin call ctools_plugin `varlist' `generate' `marker' `ifcond' `in', /// "group nkeys=`nkeys' `out' `reps' `select'" local rc = _rc } if "`marked'" != "" capture drop `marked' * Every failure, Break included, drops the unfilled output variable. if `rc' & "`generate'" != "" capture drop `generate' if `rc' == 1 exit 1 if `rc' == 134 { di as error "too many values for {bf:label define}" exit 134 } if !`rc' { local n_groups = scalar(_ctools_group_n) local group_reps `"`ctools_group_reps'"' } capture scalar drop _ctools_group_n } if `rc' & "`strict'" != "" exit `rc' if `rc' { _ctools_group_sort `varlist' `if' `in', generate(`generate') `count' `label' local n_groups = r(N_groups) local group_reps `"`r(reps)'"' } if "`generate'" != "" { if udstrlen("group(`varlist')") > 80 { note `generate' : group(`varlist') label variable `generate' "see notes" } else { label variable `generate' "group(`varlist')" } if "`label'" != "" { if `n_groups' > 0 { mata: _ctools_group_label(st_local("varlist"), st_local("group_reps"), /// st_local("generate")) } capture label list `generate' if !_rc label values `generate' `generate' } } return scalar N_groups = `n_groups' * label: the observation number of one observation of each group if "`label'" != "" return local reps `"`group_reps'"' end * Number groups by sorting in Stata (the data and their sort order are restored). program define _ctools_group_sort, rclass sortpreserve syntax varlist [if] [in], [GENerate(name) COUNT LABel] marksample touse, strok tempvar order g quietly generate long `order' = _n sort `touse' `varlist' `order' quietly by `touse' `varlist': generate long `g' = (_n == 1) if `touse' quietly replace `g' = sum(`g') if `touse' quietly summarize `g', meanonly return scalar N_groups = cond(r(N), r(max), 0) if "`label'" != "" { mata: _ctools_group_firsts(st_local("g"), st_local("order")) return local reps `"`__reps'"' } if "`generate'" != "" rename `g' `generate' end mata: /* Original observation number of the first observation of each group. */ void _ctools_group_firsts(string scalar g, string scalar order) { real colvector id, obs, previous, first id = st_data(., g) obs = st_data(., order) st_local("__reps", "") if (rows(id) == 0) return previous = (rows(id) > 1 ? (. \ id[|1 \ rows(id) - 1|]) : .) first = selectindex(id :< . :& (id :!= previous)) if (rows(first)) st_local("__reps", invtokens(strofreal(obs[first], "%12.0g")')) } /* Value label of egen group(varlist), label for groups 1..G, from one observation of each group (reps, in group order). Each variable contributes, separated by spaces: a string's value; a numeric value's label text when its value label defines one, otherwise the value ("%18.0g" for integers, "%9.0g" or "%9,0g" otherwise). Text is cut to 32 characters and trimmed, and a string's NUL bytes become \0, which is what egen group() does when called under version 16 or earlier, as the ctools ado files are. */ void _ctools_group_label(string scalar varlist, string scalar reps, string scalar lname) { real rowvector v real colvector obs, s real scalar i, j string scalar fmt, vl string colvector x, y v = st_varindex(tokens(varlist)) obs = strtoreal(tokens(reps))' if (rows(obs) > 65536) { errprintf("too many values for {bf:label define}\n") exit(134) } fmt = (st_global("c(dp)") == "comma" ? "%9,0g" : "%9.0g") for (j = 1; j <= cols(v); j++) { if (st_isnumvar(v[j])) { s = st_data(obs, v[j]) x = J(rows(s), 1, "") vl = st_varvaluelabel(v[j]) if (vl != "") { if (st_vlexists(vl)) x = ustrtrim(usubstr(st_vlmap(vl, s), 1, 32)) } for (i = 1; i <= rows(s); i++) { if (x[i] == "") x[i] = strofreal(s[i], (s[i] != trunc(s[i]) ? fmt : "%18.0g")) } } else { x = st_sdata(obs, v[j]) x = usubinstr(x, char(0), "\0", .) x = ustrtrim(usubstr(x, 1, 32)) } y = (j == 1 ? x : y :+ " " :+ x) } st_vlmodify(lname, 1::rows(y), y) } end