Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Core/Rexl.Bind/Operations/BuiltinFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,11 @@ private BuiltinFunctions()
AddOne(TextTrimFunc.TrimStart, new Sig(S.AboutTrimStart, A.Create(S.ArgSource, S.AboutTrimStart_Source)));
AddOne(TextTrimFunc.TrimEnd, new Sig(S.AboutTrimEnd, A.Create(S.ArgSource, S.AboutTrimEnd_Source)));

AddOne(TextPadFunc.PadCenter, new Sig(S.AboutPadCenter, A.Create(S.ArgSource, S.AboutPad_Source), A.Create(S.ArgLength, S.AboutPad_Len)));
AddOne(TextPadFunc.PadCenter, "PadCenter");
AddOne(TextPadFunc.PadStart, new Sig(S.AboutPadStart, A.Create(S.ArgSource, S.AboutPad_Source), A.Create(S.ArgLength, S.AboutPad_Len)));
AddOne(TextPadFunc.PadEnd, new Sig(S.AboutPadEnd, A.Create(S.ArgSource, S.AboutPad_Source), A.Create(S.ArgLength, S.AboutPad_Len)));

AddOne(TextReplaceFunc.Instance, new Sig(S.AboutTextReplace,
A.Create(S.ArgSource, S.AboutTextReplace_Source),
A.Create(S.TextReplace_Remove, S.AboutTextReplace_Remove),
Expand Down
138 changes: 138 additions & 0 deletions src/Core/Rexl.Bind/Operations/Funcs/Text.cs
Original file line number Diff line number Diff line change
Expand Up @@ -758,3 +758,141 @@ public static string Exec(string src, string remove, string insert)
return src.Replace(remove, insert);
}
}

/// <summary>
/// Functions to add padding to either end of a string.
/// The center version (PadCenter) will add spaces to both ends of the given string.
/// The start version (PadStart) will add spaces to the start of the given string.
/// The end version (PadEnd) will add spaces to the end of the given string.
/// </summary>
public sealed partial class TextPadFunc : RexlOper
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As suggested elsewhere:

  • Perhaps have a PadCenter with alias Pad that adds to both sides.
  • Use Start and End instead of Left and Right.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also register PadCenter as just Pad or perhaps make Pad the main name for it and make PadCenter be an alias?

public enum PadKind : byte
{
// Center justification, pad both ends of the string
Center,
// Pad the start of the string.
Start,
// Pad the end of the string.
End
}

public static readonly TextPadFunc PadCenter = new TextPadFunc(PadKind.Center, "Pad");
public static readonly TextPadFunc PadStart = new TextPadFunc(PadKind.Start, "PadStart");
public static readonly TextPadFunc PadEnd = new TextPadFunc(PadKind.End, "PadEnd");

public PadKind Kind { get; }

public Func<string, long, string> Map { get; }

private TextPadFunc(PadKind kind, string name)
: base(isFunc: true, new DName(name), BindUtil.TextNs, 2, 2)
{
switch (kind)
{
case PadKind.Start:
Map = ExecStart;
break;
case PadKind.End:
Map = ExecEnd;
break;
default:
Validation.Assert(kind == PadKind.Center);
Map = ExecCenter;
break;
}

Kind = kind;
}

protected override ArgTraits GetArgTraitsCore(int carg)
{
Validation.Assert(SupportsArity(carg));
var maskAll = BitSet.GetMask(carg);
var maskOpt = maskAll.ClearBit(0);
return ArgTraitsLifting.Create(this, carg, maskLiftSeq: maskAll, maskLiftTen: maskAll, maskLiftOpt: maskOpt);
}

protected override (DType, Immutable.Array<DType>) SpecializeTypesCore(InvocationInfo info)
{
Validation.AssertValue(info);
Validation.Assert(SupportsArity(info.Arity));
Validation.Assert(info.Arity == 2);

return (DType.Text, Immutable.Array.Create(DType.Text, DType.I8Req));
}

protected override bool CertifyCore(BndCallNode call, ref bool full)
{
if (call.Type != DType.Text)
return false;
var args = call.Args;
if (args[0].Type != DType.Text)
return false;
if (args[1].Type != DType.I8Req)
return false;
return true;
}

protected override BoundNode ReduceCore(IReducer reducer, BndCallNode call)
{
Validation.AssertValue(reducer);
Validation.Assert(IsValidCall(call));

var lenArg = call.Args[1];
if (lenArg.TryGetI8(out var len))
{
var srcArg = call.Args[0];
if (len <= 0)
return srcArg;
if (srcArg.TryGetString(out var str))
{
if (Util.Size(str) >= len)
return srcArg;
return BndStrNode.Create(Map(str, len));
}
}

return call;
}

public static string ExecCenter(string src, long len)
{
if (len <= 0)
return src;
int count = (int)Math.Min(len, int.MaxValue);
if (string.IsNullOrEmpty(src))
return new string(' ', count);
if (count <= src.Length)
return src;
return string.Create(count, src.AsMemory(), static (dst, mem) =>
{
int spaces = (dst.Length - mem.Length) / 2;

if (spaces > 0)
dst.Slice(0, spaces).Fill(' ');
mem.Span.CopyTo(dst.Slice(spaces, mem.Length));
dst.Slice(spaces + mem.Length).Fill(' ');
});
}

public static string ExecStart(string src, long len)
{
if (len <= 0)
return src;
int count = (int)Math.Min(len, int.MaxValue);
if (string.IsNullOrEmpty(src))
return new string(' ', count);
return src.PadLeft(count);
}

public static string ExecEnd(string src, long len)
{
if (len <= 0)
return src;
int count = (int)Math.Min(len, int.MaxValue);
if (string.IsNullOrEmpty(src))
return new string(' ', count);
return src.PadRight(count);
}
}
7 changes: 7 additions & 0 deletions src/Core/Rexl.Bind/Strings/RexlStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ static partial class RexlStrings
public static readonly StringId ArgX = new(nameof(ArgX), "x");
public static readonly StringId ArgY = new(nameof(ArgY), "y");
public static readonly StringId ArgAngle = new(nameof(ArgAngle), "angle");
public static readonly StringId ArgLength = new(nameof(ArgLength), "length");

public static readonly StringId AboutArgSeqSource = new(nameof(AboutArgSeqSource), "The source sequence.");
public static readonly StringId AboutArgPredicate = new(nameof(AboutArgPredicate), "The condition with which to test an item.");
Expand Down Expand Up @@ -342,6 +343,12 @@ static partial class RexlStrings
public static readonly StringId AboutUpper = new(nameof(AboutUpper), "Converts text to uppercase.");
public static readonly StringId AboutUpper_Source = new(nameof(AboutUpper_Source), "The text to convert to uppercase.");

public static readonly StringId AboutPadCenter = new(nameof(AboutPadCenter), "Adds spaces to both ends of the given text so the result is of the given length.");
public static readonly StringId AboutPadStart = new(nameof(AboutPadStart), "Adds spaces to the start of the given text so the result is of the given length.");
public static readonly StringId AboutPadEnd = new(nameof(AboutPadEnd), "Adds spaces to the end of the given text so the result is of the given length.");
public static readonly StringId AboutPad_Source = new(nameof(AboutPad_Source), "The text to add padding to.");
public static readonly StringId AboutPad_Len = new(nameof(AboutPad_Len), "The minimum length of the text after padding.");

public static readonly StringId AboutStartsWith = new(nameof(AboutStartsWith), "Tests whether the beginning of the source text matches the lookup text.");
public static readonly StringId AboutStartsWith_Source = new(nameof(AboutStartsWith_Source), "The text to look in.");
public static readonly StringId AboutStartsWith_LookUp = new(nameof(AboutStartsWith_LookUp), "The text to look for.");
Expand Down
7 changes: 7 additions & 0 deletions src/Core/Rexl.Bind/Strings/RexlStrings.tt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ArgValue = "value"
ArgX = "x"
ArgY = "y"
ArgAngle = "angle"
ArgLength = "length"

AboutArgSeqSource = "The source sequence."
AboutArgPredicate = "The condition with which to test an item."
Expand Down Expand Up @@ -336,6 +337,12 @@ AboutLower_Source = "The text to convert to lowercase."
AboutUpper = "Converts text to uppercase."
AboutUpper_Source = "The text to convert to uppercase."

AboutPadCenter = "Adds spaces to both ends of the given text so the result is of the given length."
AboutPadStart = "Adds spaces to the start of the given text so the result is of the given length."
AboutPadEnd = "Adds spaces to the end of the given text so the result is of the given length."
AboutPad_Source = "The text to add padding to."
AboutPad_Len = "The minimum length of the text after padding."

AboutStartsWith = "Tests whether the beginning of the source text matches the lookup text."
AboutStartsWith_Source = "The text to look in."
AboutStartsWith_LookUp = "The text to look for."
Expand Down
1 change: 1 addition & 0 deletions src/Core/Rexl.Code/Operations/BuiltinGenerators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ private BuiltinGenerators()
Add(TextPartGen.Instance);
Add(TextTrimGen.Instance);
Add(TextReplaceGen.Instance);
Add(TextPadGen.Instance);

Add(typeof(SumFunc), SumBaseGen.Instance);
Add(typeof(MeanFunc), SumBaseGen.Instance);
Expand Down
19 changes: 19 additions & 0 deletions src/Core/Rexl.Code/Operations/Funcs/Text.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,22 @@ protected override bool TryGetMeth(ICodeGen codeGen, BndCallNode call, out Metho
return true;
}
}

public sealed class TextPadGen : GetMethGen<TextPadFunc>
{
public static readonly TextPadGen Instance = new TextPadGen();

private TextPadGen()
{
}

protected override bool TryGetMeth(ICodeGen codeGen, BndCallNode call, out MethodInfo meth)
{
Validation.AssertValue(codeGen);
Validation.Assert(IsValidCall(call, true));

var fn = GetOper(call);
meth = fn.Map.Method;
return true;
}
}
85 changes: 85 additions & 0 deletions src/Test/Rexl.Bind.Test/Scripts/Binder/Functions/String.txt
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,43 @@ Text.Replace("ABC", "X", s)
Text.Replace("ABC", "B", "X")
Text.Replace("ABACDAE", "A", "!!")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be multiple kinds of test cases:

  • Test with the named globals to ensure that errors are produced on bad input types, conversions are inserted where needed (eg, using u2 for the length), arity is handled correctly.
  • Test with special values to ensure that reduction happens as expected (once ReduceCore is implemented). Use enough cases that all the code in ReduceCore and in the ExecXxx methods is covered.
  • Tests that use "opt" types in slots that lift over opt. Eg, tests that use qi8 and something like qu2 for the length.

Text.PadStart(s, i8)
Text.PadStart(null, 0)
Text.PadStart("hello", -1)
Text.PadStart(s, -1)
Text.PadStart("hello", 10)
Text.PadStart(s, 10)
Text.PadStart(s, s)
Text.PadStart(s, u2)
Text.PadStart(s, qi8)
Text.PadStart(s, qu2)
Text.PadStart(i8, i8)

Text.PadEnd(s, i8)
Text.PadEnd(null, 0)
Text.PadEnd("hello", -1)
Text.PadEnd(s, -1)
Text.PadEnd("hello", 10)
Text.PadEnd(s, 10)
Text.PadEnd(s, s)
Text.PadEnd(s, u2)
Text.PadEnd(s, qi8)
Text.PadEnd(s, qu2)
Text.PadEnd(i8, i8)

Tex.Pad(s, i8)
Text.PadCenter(s, i8)
Text.Pad(null, 15)
Text.Pad("hello", -1)
Text.Pad(s, -1)
Text.Pad("hello", 10)
Text.Pad(s, 10)
Text.Pad(s, s)
Text.Pad(s, u2)
Text.Pad(s, qi8)
Text.Pad(s, qu2)
Text.Pad(i8, i8)

// Lifting.
:: {g:g*, o:o*, s:s*, b:b*, qb:b?*, d:d*, n:n*, qn:n?*, r8:r8*, qr8:r8?*, r4:r4*, qr4:r4?*, i:i*, qi:i?*, i8:i8*, qi8:i8?*, i4:i4*, qi4:i4?*, i2:i2*, qi2:i2?*, i1:i1*, qi1:i1?*, u8:u8*, qu8:u8?*, u4:u4*, qu4:u4?*, u2:u2*, qu2:u2?*, u1:u1*, qu1:u1?*}

Expand Down Expand Up @@ -360,3 +397,51 @@ Text.Replace("ABC", s, s)
Text.Replace(s, s, s)

Text.Replace(s, "", s)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests aren't needed since they just duplicate what is above.
Tests here should be for lifting over sequence.
Also, we should add tensor based lifting tests, not just sequence.

Text.PadStart(s, i8)
Text.PadStart(null, 0)
Text.PadStart("hello", -1)
Text.PadStart("hello", [3, 4, 1])
Text.PadStart(s, [3, 4, 1])
Text.PadStart(s, -1)
Text.PadStart("hello", 10)
Text.PadStart(["hello", "howdy", null], 10)
Text.PadStart(s, 10)
Text.PadStart(s, s)
Text.PadStart(s, u1)
Text.PadStart(s, u2)
Text.PadStart(s, u4)
Text.PadStart(s, u8)
Text.PadStart(s, qi8)
Text.PadStart(s, qu2)

Text.PadEnd(s, i8)
Text.PadEnd(null, 0)
Text.PadEnd(s, -1)
Text.PadEnd(s, [3, 4, 1])
Text.PadEnd("hello", [3, 4, 9])
Text.PadEnd(["hello", "howdy", null], 10)
Text.PadEnd(s, 10)
Text.PadEnd(s, s)
Text.PadEnd(s, u1)
Text.PadEnd(s, u2)
Text.PadEnd(s, u4)
Text.PadEnd(s, u8)
Text.PadEnd(s, qi8)
Text.PadEnd(s, qu2)

Text.PadCenter(s, i8)
Text.PadCenter(null, 0)
Text.PadCenter("hello", -1)
Text.PadCenter("hello", [3, 4, 9])
Text.PadCenter(s, -1)
Text.PadCenter(s, [3, 4, 1])
Text.PadCenter(["hello", "howdy", null], 10)
Text.PadCenter(s, 10)
Text.PadCenter(s, s)
Text.PadCenter(s, u1)
Text.PadCenter(s, u2)
Text.PadCenter(s, u4)
Text.PadCenter(s, u8)
Text.PadCenter(s, qi8)
Text.PadCenter(s, qu2)
19 changes: 18 additions & 1 deletion src/Test/Rexl.Code.Test/Scripts/CodeGen/Functions/String.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
::: {g:g, o:o, s:s, b:b, qb:b?, d:d, qd:d?, n:n, qn:n?, r8:r8, qr8:r8?, r4:r4, qr4:r4?, i:i, qi:i?, i8:i8, qi8:i8?, i4:i4, qi4:i4?, i2:i2, qi2:i2?, i1:i1, qi1:i1?, u8:u8, qu8:u8?, u4:u4, qu4:u4?, u2:u2, qu2:u2?, u1:u1, qu1:u1?}

``` N := First([null, "hello"]);
``` N := Null("hello");

Text.Part(Wrap(""), 0)
Text.Part(Wrap(""), 1)
Expand Down Expand Up @@ -71,3 +71,20 @@ Text.TrimEnd(Wrap(Null(""))) | With(_, {S: it, L: Text.Len(it)})
Text.Replace(N, "A", "B")
Text.Replace("A", N, "B")
Text.Replace("ABC", Wrap("B"), N)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also have:

  • Tests for lifting.
  • Tests of cases that aren't reduced during binding (once ReduceCore is implemented). That's the purpose of using Wrap.

Text.PadStart(N, 0)
Text.PadStart(N, Wrap(10))
Text.PadStart("hello", Wrap(-1))
Text.PadStart("hello", Wrap(10))

Text.PadEnd(N, 0)
Text.PadEnd(N, Wrap(10))
Text.PadEnd("hello", -1)
Text.PadEnd("hello", 10)
Text.PadEnd(Wrap("hello"), 10)

Text.Pad(N, 0)
Text.Pad(N, Wrap(10))
Text.PadCenter("hello", -1)
Text.Pad("hello", 10)
Text.Pad(Wrap("hello"), 10)