*!Version 2.0 23APR2020 SurveyCTO support@surveycto.com

/* Developed by William Blackmon (wblackmon@poverty-action.org) - IPA (Innovations for Poverty Action)
  This program shifts time zones for specified time or datetime fields collected by SurveyCTO.
*/

cap program drop sctorezone
program sctorezone
	syntax anything(id="time zone shift or starttime variable" name=shift) [if] [in], force [Only(varlist) | Exclude(varlist)]

 	* break "shifted" into direction (+ or -) and hours

		// is "shift" in manual or automatic form?
		cap confirm string variable `shift'
		if _rc == 0 loc shifttype = "automatic"
		if _rc >  0 loc shifttype = "manual"

		tempvar shifttime

		// manual: +/-X
		if "`shifttype'" == "manual" {
			if substr("`shift'",1,1)!="+" & substr("`shift'",1,1)!="-" {
				di as error "Shift must either be a string variable or in the form '+3' or '-4'."
				error 1
			}

			loc direction = substr("`shift'", 1, 1)
			gen `shifttime' = real(subinstr("`shift'","`direction'", "", 1))
			if "`direction'" == "-" qui replace `shifttime' = `shifttime'*-1
			cap assert !mi(`shifttime')
			if _rc > 0 {
				di as error "Shift must either be a string variable or in the form '+3' or '-4'."
				error 1
			}
		}

		// automatic: starttime variable
		if "`shifttype'" == "automatic" {
			cap assert !mi(`shift')
			if _rc > 0 {
				di as error "Shift variable must never be missing."
				error 1
			}
			tempvar localraw
			tempvar shiftraw
			gen double `localraw' = clock(`shift', "YMDhms")
			gen double `shiftraw' = `localraw' - starttime
			gen double `shifttime' = `shiftraw'/60/60/1000
		}

	* error handling
		// input both `only' and `exclude'
		if "`only'"!="" & "`exclude'"!="" {
			di as error `"Use the "only" or "exclude" options (or neither), not both."'
			error 1
		}

		// shift issues
		qui count if mi(`shifttime')
		if r(N)>0 {
			di as error "Shift must either be a string variable which is never missing or in the form '+3' or '-4'."
			error 1
		}
		qui count if `shifttime'<=-24 | `shifttime'>=24
		if r(N)>0 {
			di as error "Time zone shift must be in hours, between +/-24 (exclusive)."
			error 1
		}


	* create varlists that will have their time zones adjusted

		// normal (no only or exclude options used)
		if "`only'`exclude'"=="" {
			unab all : _all
		}

		// only option used
		if "`only'" != "" {
			unab all : `only'
		}


		// exclude option used
		if "`exclude'" != "" {
			unab all : _all
			loc all = " `all' "
			foreach var of varlist `exclude' {
				loc all = subinstr("`all'", " `var' ", " ", 1)
			}
		}

		// create separate varlists for each type: time and datetime

			// time: string variable that always ends in " AM" or " PM" if not missing
			qui ds `all', has(type string)
			if "`r(varlist)'" != "" {
				foreach var of varlist `r(varlist)' { // loop over all string variables
					cap: assert mi(`var') | strmatch(`var',"*:??:?? ?M") // must always be missing or follow SCTO time format
					if _rc == 0 {
						qui count if mi(`var') // don't change if always missing
						if r(N)<`=_N' loc tvars = "`tvars' `var'" // add to list of time variables
					}
				}
			}

			// datetime: uses the tc variable format
			qui ds `all', has(format %tc*)
			loc dtvars = r(varlist)
			if "`dtvars'"=="." loc dtvars = ""

	* adjust time zone of time variables
		if "`tvars'" != "" {
		foreach var of varlist `tvars' {
				qui {
					if regexm(string(`shifttime'),"[.][0-9]$") {
						tempvar secs
						gen `secs' = substr(`var', -5,2) // in "12:34:56 AM", just "56"
						tempvar rawmins
						gen `rawmins' = substr(`var', -8,2) // in "12:34:56 AM", just "34"
						tempvar shiftmins
						gen `shiftmins'= real(substr(string(`shifttime'),-1,1))*6
						if "`direction'" == "-" qui replace `shiftmins' = `shiftmins'*-1
						tempvar mins
						gen `mins' = real(`rawmins') + `shiftmins'
						replace `mins' = `mins' - 60 if `mins'>=60
						replace `mins' = `mins' + 60 if `mins'<0
						tostring `mins', force replace
						replace `mins' = "0" + `mins' if strlen(`mins')==1
						tempvar rawhours
						gen `rawhours' = real(subinstr(substr(`var',1,2),":","",1)) // hours only, 1-12
						tempvar shifthours
						gen `shifthours'= real(substr(string(`shifttime'), 1, strpos(string(`shifttime'), ".")-1))
						replace `rawhours' = `rawhours' + 12 if substr(`var',-2,.)=="PM" & `rawhours'<12 // changing 1PM-11PM to 13-23
						replace `rawhours' = `rawhours' + `shifthours' // time zone shift
						replace `rawhours' = `rawhours' - 24 if `rawhours'>=24 // if shift moves time to the next day, remove 24 hours
						replace `rawhours' = `rawhours' + 24 if `rawhours'<0 // if shift moved time to the previous day, add 24 hours
						tempvar hours12 // hours component of shifted time, 1-12
						gen `hours12' = `rawhours'
						replace `hours12' = `rawhours'-12 if `rawhours'>12 // changing 13-23 back to 1PM-11:PM
						replace `var' = string(`hours12') + ":" + `mins' + ":" + `secs' + " " if !mi(`var') // adding in formatting
						replace `var' = `var' + "AM" if `rawhours'<12 & !mi(`var') // add AM if raw hours between 0-11
						replace `var' = `var' + "PM" if `rawhours'>=12 & !mi(`var') // add PM if raw hours between 12-23
					}
					else if regexm(string(`shifttime'),"[.][0-9]$")==0 {
						tempvar minsecs
						gen `minsecs' = substr(`var', -8,5) // in "12:34:56 AM", just "34:56"
						tempvar rawhours
						gen `rawhours' = real(subinstr(substr(`var',1,2),":","",1)) // hours only, 1-12
						replace `rawhours' = `rawhours' + 12 if substr(`var',-2,.)=="PM" & `rawhours'<12 // changing 1PM-11PM to 13-23
						replace `rawhours' = `rawhours' + `shifttime' // time zone shift
						replace `rawhours' = `rawhours' - 24 if `rawhours'>=24 // if shift moves time to the next day, remove 24 hours
						replace `rawhours' = `rawhours' + 24 if `rawhours'<0 // if shift moved time to the previous day, add 24 hours
						tempvar hours12 // hours component of shifted time, 1-12
						gen `hours12' = `rawhours'
						replace `hours12' = `rawhours'-12 if `rawhours'>12 // changing 13-23 back to 1PM-11:PM
						replace `var' = string(`hours12') + ":" + `minsecs' + " " if !mi(`var') // adding in formatting
						replace `var' = `var' + "AM" if `rawhours'<12 & !mi(`var') // add AM if raw hours between 0-11
						replace `var' = `var' + "PM" if `rawhours'>=12 & !mi(`var') // add PM if raw hours between 12-23
					}
			}
		}
	}


	* adjust time zone of datetimetime variables
	if "`dtvars'" != "" {
		foreach var of varlist `dtvars' {
			qui replace `var' = `var' + `shifttime'*60*60*1000
		}
	}

	* display results
		loc numvars = wordcount("`tvars' `dtvars'")
		if `numvars'==0 di "No variables have been shifted."
		if `numvars'>0 {
			di "The following variable(s) have been shifted: `tvars' `dtvars'"
			qui levelsof `shifttime', loc(shiftlevels)
			foreach sh of local shiftlevels {
				qui count if `shifttime' == `sh'
				loc s
				loc fb "backwards"
				if `sh'>0 loc fb "forwards"
				loc abssh = abs(`sh')
				if `abssh'>1 loc s = "s"
				if `sh'!=0 di "- `r(N)' observations shifted `fb' by `abssh' hour`s'."
				if `sh'==0 di "- `r(N)' observations not shifted."
			}
		}
end