Created
October 30, 2023 18:02
-
-
Save erkrali/d48ed754b68a26e326b61aad78d43ab7 to your computer and use it in GitHub Desktop.
mixin-based string interpolation in D with implicit conversion to string
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module interpolation; | |
import std.meta; | |
public: | |
private struct InterpolatedBlockTag { } | |
auto interpolatedBlock(string _preText, string _format, string _originalSymbol, T)(auto ref T val) { | |
static struct InterpolatedBlock { | |
public: | |
alias Tag = InterpolatedBlockTag; | |
alias preText = _preText; | |
alias format = _format; | |
alias originalSymbol = _originalSymbol; | |
alias tuple = AliasSeq!(preText, format, val); | |
T val; | |
string toString()() const { | |
import std.conv; | |
return(preText ~ val.to!string); | |
} | |
alias toString this; | |
private: | |
this(T val) { | |
this.val = val; | |
} | |
} | |
return InterpolatedBlock(val); | |
} | |
template isInterpolatedBlock(alias T) { | |
static if (is(T)) { | |
enum isInterpolatedBlock = is(T.Tag == InterpolatedBlockTag); | |
} else { | |
enum isInterpolatedBlock = is(typeof(T).Tag == InterpolatedBlockTag); | |
} | |
} | |
unittest { | |
auto block = interpolatedBlock!("Hello, ", "", "name")("John"); | |
static assert(isInterpolatedBlock!block); | |
assert(block == "Hello, John"); | |
static assert(block.preText == "Hello, "); | |
static assert(block.format == ""); | |
assert(block.val == "John"); | |
static assert(block.originalSymbol == "name"); | |
} | |
private struct InterpolatedStringTag { } | |
auto interpolatedString(InterpolatedBlocks...)(InterpolatedBlocks blocks) { | |
static assert (allSatisfy!(isInterpolatedBlock,InterpolatedBlocks), "You can only construct an interpolated string out of interpolated blocks. Got ", InterpolatedBlocks); | |
static struct InterpolatedString { | |
public: | |
alias Tag = InterpolatedStringTag; | |
InterpolatedBlocks blocks; | |
static if (InterpolatedBlocks.length > 0) { | |
@disable this(); | |
} | |
@disable this(this); | |
alias toString this; | |
string toString()() const { | |
string result; | |
static foreach(block;blocks) { | |
result ~= block.toString; | |
} | |
return result; | |
} | |
template _tuple() { | |
alias _tuple = AliasSeq!(); | |
static foreach(i; 0..blocks.length) { | |
_tuple = AliasSeq!(_tuple, InterpolatedBlocks[i].preText, InterpolatedBlocks[i].format, vals[i]); | |
} | |
} | |
alias tuple = _tuple!(); | |
private: | |
static if (InterpolatedBlocks.length > 0) { | |
this(InterpolatedBlocks blocks) { | |
foreach (i, block ; blocks) { | |
this.blocks[i] = block; | |
this.vals[i] = block.val; | |
} | |
} | |
} | |
staticMap!(valType, InterpolatedBlocks) vals; | |
template valType(InterpolatedBlock) if (isInterpolatedBlock!InterpolatedBlock){ | |
alias valType = typeof(InterpolatedBlock.val); | |
} | |
} | |
return InterpolatedString(blocks); | |
} | |
template isInterpolatedString(alias T) { | |
static if (is (T)) { | |
enum isInterpolatedString = is(T.Tag == InterpolatedStringTag); | |
} else { | |
enum isInterpolatedString = is(typeof(T).Tag == InterpolatedStringTag); | |
} | |
} | |
unittest { | |
import std.logger; | |
auto iString = interpolatedString( | |
interpolatedBlock!("Hello, ", "", "firstName")("John"), | |
interpolatedBlock!(" ", "", "lastName")("Smith") | |
); | |
static assert(isInterpolatedString!iString); | |
assert(iString == "Hello, John Smith"); | |
static assert(iString.tuple[0] == "Hello, "); | |
static assert(iString.tuple[1] == ""); | |
assert(iString.blocks[0].tuple[2] == "John"); | |
static assert(iString.blocks[0].originalSymbol == "firstName"); | |
static assert(iString.tuple[3] == " "); | |
static assert(iString.tuple[4] == ""); | |
assert(iString.tuple[5] == "Smith"); | |
static assert(iString.blocks[1].originalSymbol == "lastName"); | |
} | |
private string interpolate(string s)() { | |
import std.logger; | |
string output = "interpolatedString("; | |
int bracketDepth = 0; | |
int formatBracketDepth = 0; | |
string preText = ""; | |
string format = ""; | |
string originalSymbol = ""; | |
string current; | |
for(int i = 0; i < s.length; i++) { | |
if (s[i] == '\\') { | |
i++; | |
assert (i < s.length, "Unterminated escape sequence"); | |
current ~= s[i]; | |
} else if (bracketDepth == 0 && i + 1 < s.length && s[i] == '$' && s[i+1] == '(') { // "$(" starts the interpolation | |
preText = current; | |
current = ""; | |
i++; | |
bracketDepth = 1; | |
if (i + 1 < s.length && s[i+1] == '{') { // "$({" starts a formatted interpolation | |
formatBracketDepth = 1; | |
i++; | |
} | |
} else if (bracketDepth > 0 && formatBracketDepth == 0 && s[i] == '(') { | |
bracketDepth++; | |
current ~= '('; | |
} else if (formatBracketDepth > 0 && s[i] == '{') { | |
formatBracketDepth++; | |
current ~= '{'; | |
} else if (formatBracketDepth > 0 && s[i] == '}') { | |
formatBracketDepth--; | |
if (formatBracketDepth == 0) { | |
format = current; | |
current = ""; | |
} else { | |
current ~= '}'; | |
} | |
} else if (bracketDepth > 0 && formatBracketDepth == 0 && s[i] == ')') { | |
bracketDepth--; | |
if (bracketDepth == 0) { | |
originalSymbol = current; | |
current = ""; | |
output ~= "interpolatedBlock!(\"" ~ | |
preText ~ "\",\"" ~ | |
format ~ "\", \"" ~ | |
originalSymbol ~ "\")(" ~ | |
originalSymbol ~ "),"; | |
preText = ""; | |
format = ""; | |
originalSymbol = ""; | |
} else { | |
current ~= ')'; | |
} | |
} else { | |
current ~= s[i]; | |
} | |
} | |
assert(bracketDepth == 0 && formatBracketDepth == 0, "Improperly terminated interpolation string."); | |
if (preText != "" || format != "" || originalSymbol != "") { | |
output ~= "interpolatedBlock!(\"" ~ | |
preText ~ "\",\"" ~ | |
format ~ "\", \"" ~ | |
originalSymbol ~ "\")(" ~ | |
originalSymbol ~ "),"; | |
} | |
output ~= ")"; | |
return output; | |
} | |
enum i(string s) = interpolate!s; | |
enum iS(string s) = interpolate!s ~ ".toString"; | |
enum it(string s) = interpolate!s ~ ".tuple"; | |
unittest { | |
static assert (i!"" == `interpolatedString()`); | |
static assert (i!"Hello, $({%30s}name)" == `interpolatedString(interpolatedBlock!("Hello, ","%30s", "name")(name),)`); | |
static assert (i!"Hello, $({{%30x)}}(name))" == `interpolatedString(interpolatedBlock!("Hello, ","{%30x)}", "(name)")((name)),)`); | |
assert(mixin(i!``) == ""); | |
string firstName = "John"; | |
string lastName = "Smith"; | |
string greeting = mixin(i!`Hello, $(firstName) $(lastName)`); | |
assert(greeting == "Hello, John Smith"); | |
auto greetingRaw = mixin(i!`Hello, $(firstName) $(lastName)`); | |
static assert(isInterpolatedString!greetingRaw); | |
() @nogc { | |
int ii = 1; | |
assert(mixin(it!`The number is $(ii)`) == AliasSeq!("The number is ", "", 1)); | |
}(); | |
static struct S { | |
int i; | |
static numCalls = 0; | |
this(int i) { | |
this.i = i; | |
S.numCalls++; | |
} | |
string toString() const { | |
import std.conv : to; | |
return i.to!string; | |
} | |
} | |
assert(mixin(i!`The number is $(S(1))`) == "The number is 1"); | |
assert(S.numCalls == 1); | |
struct S2 { | |
import std.traits; | |
static string fun(T)(T s) if (!isInterpolatedString!(T) && isSomeString!T) { | |
return "string"; | |
} | |
static string fun(T)(T t) if (isInterpolatedString!(T)) { | |
return "interpolated"; | |
} | |
} | |
assert(S2.fun("") == "string"); | |
assert(S2.fun(mixin(i!``)) == "interpolated"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment