diff --git a/src/Core/Rexl.Bind/Operations/BuiltinFunctions.cs b/src/Core/Rexl.Bind/Operations/BuiltinFunctions.cs index ef089a2..2fdd4d2 100644 --- a/src/Core/Rexl.Bind/Operations/BuiltinFunctions.cs +++ b/src/Core/Rexl.Bind/Operations/BuiltinFunctions.cs @@ -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), diff --git a/src/Core/Rexl.Bind/Operations/Funcs/Text.cs b/src/Core/Rexl.Bind/Operations/Funcs/Text.cs index e169e6d..52e5fd2 100644 --- a/src/Core/Rexl.Bind/Operations/Funcs/Text.cs +++ b/src/Core/Rexl.Bind/Operations/Funcs/Text.cs @@ -758,3 +758,141 @@ public static string Exec(string src, string remove, string insert) return src.Replace(remove, insert); } } + +/// +/// 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. +/// +public sealed partial class TextPadFunc : RexlOper +{ + 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 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) 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); + } +} diff --git a/src/Core/Rexl.Bind/Strings/RexlStrings.cs b/src/Core/Rexl.Bind/Strings/RexlStrings.cs index bf86c7b..299536a 100644 --- a/src/Core/Rexl.Bind/Strings/RexlStrings.cs +++ b/src/Core/Rexl.Bind/Strings/RexlStrings.cs @@ -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."); @@ -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."); diff --git a/src/Core/Rexl.Bind/Strings/RexlStrings.tt b/src/Core/Rexl.Bind/Strings/RexlStrings.tt index 84c5af0..488b9da 100644 --- a/src/Core/Rexl.Bind/Strings/RexlStrings.tt +++ b/src/Core/Rexl.Bind/Strings/RexlStrings.tt @@ -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." @@ -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." diff --git a/src/Core/Rexl.Code/Operations/BuiltinGenerators.cs b/src/Core/Rexl.Code/Operations/BuiltinGenerators.cs index 8f3b13e..e3658f8 100644 --- a/src/Core/Rexl.Code/Operations/BuiltinGenerators.cs +++ b/src/Core/Rexl.Code/Operations/BuiltinGenerators.cs @@ -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); diff --git a/src/Core/Rexl.Code/Operations/Funcs/Text.cs b/src/Core/Rexl.Code/Operations/Funcs/Text.cs index 7cd8827..514050b 100644 --- a/src/Core/Rexl.Code/Operations/Funcs/Text.cs +++ b/src/Core/Rexl.Code/Operations/Funcs/Text.cs @@ -293,3 +293,22 @@ protected override bool TryGetMeth(ICodeGen codeGen, BndCallNode call, out Metho return true; } } + +public sealed class TextPadGen : GetMethGen +{ + 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; + } +} diff --git a/src/Test/Rexl.Bind.Test/Scripts/Binder/Functions/String.txt b/src/Test/Rexl.Bind.Test/Scripts/Binder/Functions/String.txt index 6e89e64..8b69a23 100644 --- a/src/Test/Rexl.Bind.Test/Scripts/Binder/Functions/String.txt +++ b/src/Test/Rexl.Bind.Test/Scripts/Binder/Functions/String.txt @@ -238,6 +238,43 @@ Text.Replace("ABC", "X", s) Text.Replace("ABC", "B", "X") Text.Replace("ABACDAE", "A", "!!") +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?*} @@ -360,3 +397,51 @@ Text.Replace("ABC", s, s) Text.Replace(s, s, s) Text.Replace(s, "", s) + +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) diff --git a/src/Test/Rexl.Code.Test/Scripts/CodeGen/Functions/String.txt b/src/Test/Rexl.Code.Test/Scripts/CodeGen/Functions/String.txt index 3705ef1..d12f089 100644 --- a/src/Test/Rexl.Code.Test/Scripts/CodeGen/Functions/String.txt +++ b/src/Test/Rexl.Code.Test/Scripts/CodeGen/Functions/String.txt @@ -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) @@ -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) + +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)