/*-------------------------------------------------------------------------------
  bracketfill.ado: fill a continuous variable with coarse data from a bracket variable

    Copyright (C) 2020-2021  Daniel Bela (daniel.bela@lifbi.de)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

-------------------------------------------------------------------------------*/
*! bracketfill.ado: fill a continuous variable with coarse data from a bracket variable
*! Daniel Bela (daniel.bela@lifbi.de), Leibniz Institute for Educational Trajectories (LIfBi), Germany
*! version 1.1 29 April 2021 - updated documentation
*! version 1.0 23 October 2020 - initial release
program define bracketfill , nclass
	version 11.2
	// prepare macros
	local syntaxerror 0
	local bracketcount 0
	local nextbracket 1
	local valid_functions `""median","mean","center""'
	tempvar targetvar indicatorvar
	tempname resultscalar maxscalar
	// parse syntax
	syntax anything(equalok name=expression id="=exp") /// varname={median|mean|center}
		[if/] /// if-condition to determine observations to be filled, if any
		[in] /// in-condition to determine observations to be filled, if any
		, ///
		brackets(string asis) ///
		[ ///
			replace ///
			Verbose ///
			COPYrest(varname) ///
			Missing(numlist integer min=1 max=1 missingokay) ///
			Exclude(string) ///
		]
	// prepare -if- condition
	if (!missing(`"`if'"')) {
		local ifaddon `" & `if'"'
	}
	// prepare if expression for exclude
	if (!missing(`"`exclude'"')) {
		local excludeaddon `" & !(`exclude')"'
	}
	// parse -expression-
	gettoken resultvar rest : expression , parse("=")
	if (`"`replace'"'==`"replace"') confirm variable `resultvar' , exact
	else confirm new variable `resultvar' , exact
	if (missing(`"`rest'"')) local syntaxerror 1
	else {
		gettoken equalsign function : rest, parse("=")
		if (`"`equalsign'"'!=`"="') local syntaxerror 1
		if (missing("`function'")) local syntaxerror 1
	}
	if (!inlist(`"`function'"',`valid_functions')) local syntaxerror 1
	// parse -brackets-
	while (!missing(ustrtrim(stritrim(`"`brackets'"')))) {
		if (`nextbracket'==1) {
			local nextbracket 0
			local ++ bracketcount
			local target code
		}
		else {
			local nextbracket 1
			local target range
		}
		gettoken bracket`bracketcount'_`target' brackets : brackets
		if (missing(ustrtrim(stritrim(`"`brackets'"')))) & (`"`target'"'!=`"range"') {
			display as error in smcl `"invalid {it:bracketspec}"'
			local syntaxerror 1
			continue , break
		}
		if (`"`target'"'!=`"range"') continue
		local first_operator=ustrpos(`"`bracket`bracketcount'_`target''"',`"<"')
		if (`first_operator'==0) {
			local syntaxerror 1
			continue , break
		}
		local bracket`bracketcount'_lowerbound=usubstr(`"`bracket`bracketcount'_`target''"',1,`first_operator'-1)
		local bracket`bracketcount'_lowerbound_opr ">"
		if (usubstr(`"`bracket`bracketcount'_`target''"',`first_operator'+1,1)==`"="') {
			local bracket`bracketcount'_lowerbound_inc "["
			local bracket`bracketcount'_lowerbound_opr ">="
			local reststart=`first_operator'+2
		}
		else {
			local bracket`bracketcount'_lowerbound_inc "]"
			local reststart=`first_operator'+1
		}
		local rest=usubstr(`"`bracket`bracketcount'_`target''"',`reststart',.)
		local second_operator=ustrpos(`"`rest'"',`"<"')
		if (`second_operator'==0) {
			local syntaxerror 1
			continue , break
		}
		local bracket`bracketcount'_focusvar=usubstr(`"`rest'"',1,`second_operator'-1)
		confirm numeric variable `bracket`bracketcount'_focusvar'
		local bracket`bracketcount'_upperbound_opr "<"
		if (usubstr(`"`rest'"',`second_operator'+1,1)==`"="') {
			local bracket`bracketcount'_upperbound_inc "]"
			local bracket`bracketcount'_upperbound_opr "<="
			local upperboundstart=`second_operator'+2
		}
		else {
			local bracket`bracketcount'_upperbound_inc "["
			local upperboundstart=`second_operator'+1
		}
		local bracket`bracketcount'_upperbound=usubstr(`"`rest'"',`upperboundstart',.)
	}
	// set local noisily to "noisily" , depending on option -verbose-
	if (`"`verbose'"'=="verbose") local noisily noisily
	// set default for -missing-, if not specified
	if (missing(`"`missing'"')) local missing .
	// abort with error, if appropriate
	if (`syntaxerror'==1) syntaxerror
	// create indicator variable for already replaced/filled observations
	quietly : generate `indicatorvar'=0
	// create target variable
	quietly : generate `targetvar'=`missing'
	// calculate and insert values for each bracket
	forvalues num=1/`bracketcount' {
		display _newline as result in smcl `"{text}inserting {result:`function'} for bracket {result:`num'}..."'
		local bracketrange "(`bracket`num'_focusvar'`bracket`num'_lowerbound_opr'`bracket`num'_lowerbound' &  `bracket`num'_focusvar'`bracket`num'_upperbound_opr'`bracket`num'_upperbound')`excludeaddon'"
		quietly : count if (`bracketrange')
		local s=cond(`r(N)'==1,"","s")
		if (`"`verbose'"'==`"verbose"') display as result in smcl `"{text}{tab}...{result:`r(N)'} observation`s' in bracket range {input:`bracket`num'_lowerbound_inc'`bracket`num'_lowerbound',`bracket`num'_upperbound'`bracket`num'_upperbound_inc'} for variable {input:`bracket`num'_focusvar'}"'
		if (`"`function'"'==`"median"') {
			quietly : summarize `bracket`num'_focusvar' if (`bracketrange') , detail
			scalar define `resultscalar'=r(p50)
		}
		else if (`"`function'"'==`"mean"') {
			summarize `bracket`num'_focusvar' if (`bracketrange') , meanonly
			scalar define `resultscalar'=r(mean)
		}
		else if (`"`function'"'==`"center"') {
			if (missing(`bracket`num'_upperbound')) {
				summarize `bracket`num'_focusvar' if (`bracketrange') , meanonly
				scalar define `maxscalar'=r(max)
				if (`"`verbose'"'==`"verbose"') display as result in smcl `"{text}{tab}...upper bound for bracket is {input:missing}; will use empirical maximum value {result}"' `maxscalar' `"{text} instead to calculate bracket's center"'
			}
			else scalar define `maxscalar'=`bracket`num'_upperbound'
			scalar define `resultscalar'=`bracket`num'_lowerbound'+((`maxscalar'-`bracket`num'_lowerbound')/2)
		}
		else {
			/* not reached */
			error 459
		}
		if (`"`verbose'"'==`"verbose"') display as result in smcl `"{text}{tab}...calculated {input:`function'} is: {result}"' `resultscalar'
		replace `targetvar'=`resultscalar' `in' if ((`bracket`num'_code') & `indicatorvar'==0`ifaddon')
		quietly : replace `indicatorvar'=1 `in' if ((`bracket`num'_code') & `indicatorvar'==0`ifaddon')
	}
	// execute -copyrest-, if specified
	if (!missing(`"`copyrest'"')) {
		quietly : replace `targetvar'=`copyrest' `in' if (`indicatorvar'==0)
		quietly : replace `indicatorvar'=1 `in' if (`indicatorvar'==0)
	}
	// migrate to final variable name (or replace into existing variable, if -replace- is specified)
	if (`"`replace'"'==`"replace"') replace `resultvar'=`targetvar' if (`indicatorvar'==1)
	else rename `targetvar' `resultvar'
	exit 0
end
program define syntaxerror , nclass
	display as error in smcl `"Syntax error; valid syntax is:"'
	display as error in smcl `"{tab}{input:bracketfill newvarname={it:function} , brackets({it:bracketspecs}) [options]}"'
	display as error in smcl `"{tab}with {input:median}, {input:mean}, or {input:center} as possible functions; see {help bracketfill:help file} for details."'
	exit 198
end
// EOF