JavaScript (ES6), 5252 49 bytes
s=>s.replacef=s=>s&&(/s./g,s=>charCodeAt()+4+f(s.charCodeAtslice(1)+4+"")).slice(1))
Recursion seemsturned out to be 23 bytes longer in 2 different waysshorter than .replace
:
f=([c,...s])=>c?(cs=>s.charCodeAtreplace()+4+"")/.slice(1)+f(s):""
f=s=>s?/g,s=>(s.charCodeAt()+4+"").slice(1)+f(s.slice(1)):""
parseInt(s,36)
is 1-4 bytesslightly longer for each approach, because you have to change 4
to 91
:
s=>s.replace(/./g,s=>(s.charCodeAt()+4+"").slice(1))
f=([c,...s])=>c?(parseInt(cs,36)+91+"").slice(1)+f(s):""
f=s=>s?f=s=>s&&(parseInt(s[0],36)+91+"")+91+f(s.slice(1)+f(s)).slice(1)):""