diff --git a/docs/text.md b/docs/text.md index cedc8d5..79fc5dd 100644 --- a/docs/text.md +++ b/docs/text.md @@ -290,6 +290,9 @@ pattern documentation](patterns.md) for more details. - [`func has(text: Text, pattern: Pattern -> Bool)`](#has) - [`func join(glue: Text, pieces: [Text] -> Text)`](#join) - [`func split(text: Text -> [Text])`](#lines) +- [`func middle_pad(text: Text, width: Int, pad: Text = " " -> Text)`](#middle_pad) +- [`func left_pad(text: Text, width: Int, pad: Text = " " -> Text)`](#left_pad) +- [`func lines(text: Text, pattern: Pattern = "" -> [Text])`](#lines) - [`func lower(text: Text, language: Text = "C" -> Text)`](#lower) - [`func map(text: Text, pattern: Pattern, fn: func(text:Match)->Text -> Text, recursive: Bool = yes)`](#map) - [`func matches(text: Text, pattern: Pattern -> [Text])`](#matches) @@ -298,8 +301,8 @@ pattern documentation](patterns.md) for more details. - [`func replace(text: Text, pattern: Pattern, replacement: Text, backref: Pattern = $/\/, recursive: Bool = yes -> Text)`](#replace) - [`func replace_all(replacements:{Pattern,Text}, backref: Pattern = $/\/, recursive: Bool = yes -> Text)`](#replace_all) - [`func reversed(text: Text -> Text)`](#reversed) +- [`func right_pad(text: Text, width: Int, pad: Text = " " -> Text)`](#right_pad) - [`func slice(text: Text, from: Int = 1, to: Int = -1 -> Text)`](#slice) -- [`func split(text: Text, pattern: Pattern = "" -> [Text])`](#split) - [`func starts_with(text: Text, prefix: Text -> Bool)`](#starts_with) - [`func title(text: Text, language: Text = "C" -> Text)`](#title) - [`func to(text: Text, last: Int -> Text)`](#to) @@ -777,12 +780,64 @@ A single `Text` value with the pieces joined by the glue. --- +### `middle_pad` +Pad some text on the left and right side so it reaches a target width. + +```tomo +func middle_pad(text: Text, width: Int, pad: Text = " " -> Text) +``` + +- `text`: The text to pad. +- `width`: The target width. +- `pad`: The padding text (default: `" "`). + +**Returns:** +Text with length at least `width`, with extra padding on the left and right as +needed. If `pad` has length greater than 1, it may be partially repeated to +reach the exact desired length. + +**Example:** +```tomo +>> "x":middle_pad(6) += " x " +>> "x":middle_pad(10, "ABC") += "ABCAxABCAB" +``` + +--- + +### `left_pad` +Pad some text on the left side so it reaches a target width. + +```tomo +func left_pad(text: Text, width: Int, pad: Text = " " -> Text) +``` + +- `text`: The text to pad. +- `width`: The target width. +- `pad`: The padding text (default: `" "`). + +**Returns:** +Text with length at least `width`, with extra padding on the left as needed. If +`pad` has length greater than 1, it may be partially repeated to reach the +exact desired length. + +**Example:** +```tomo +>> "x":left_pad(5) += " x" +>> "x":left_pad(5, "ABC") += "ABCAx" +``` + +--- + ### `lines` Splits the text into an array of lines of text, preserving blank lines, ignoring trailing newlines, and handling `\r\n` the same as `\n`. ```tomo -func split(text: Text -> [Text]) +func lines(text: Text -> [Text]) ``` - `text`: The text to be split into lines. @@ -1052,6 +1107,32 @@ A reversed version of the text. --- +### `right_pad` +Pad some text on the right side so it reaches a target width. + +```tomo +func right_pad(text: Text, width: Int, pad: Text = " " -> Text) +``` + +- `text`: The text to pad. +- `width`: The target width. +- `pad`: The padding text (default: `" "`). + +**Returns:** +Text with length at least `width`, with extra padding on the right as needed. If +`pad` has length greater than 1, it may be partially repeated to reach the +exact desired length. + +**Example:** +```tomo +>> "x":right_pad(5) += "x " +>> "x":right_pad(5, "ABC") += "xABCA" +``` + +--- + ### `slice` Get a slice of the text. diff --git a/environment.c b/environment.c index f283e69..259cdd7 100644 --- a/environment.c +++ b/environment.c @@ -409,15 +409,18 @@ env_t *new_compilation_unit(CORD libname) {"without_escaping", "Path$cleanup", "func(text:Text -> Path)"}, {"has", "Text$has", "func(text:Text, pattern:Pattern -> Bool)"}, {"join", "Text$join", "func(glue:Text, pieces:[Text] -> Text)"}, + {"left_pad", "Text$left_pad", "func(text:Text, count:Int, pad=\" \" -> Text)"}, {"lines", "Text$lines", "func(text:Text -> [Text])"}, {"lower", "Text$lower", "func(text:Text, language=\"C\" -> Text)"}, {"map", "Text$map", "func(text:Text, pattern:Pattern, fn:func(match:Match -> Text), recursive=yes -> Text)"}, {"matches", "Text$matches", "func(text:Text, pattern:Pattern -> [Text]?)"}, + {"middle_pad", "Text$middle_pad", "func(text:Text, count:Int, pad=\" \" -> Text)"}, {"quoted", "Text$quoted", "func(text:Text, color=no -> Text)"}, {"repeat", "Text$repeat", "func(text:Text, count:Int -> Text)"}, {"replace", "Text$replace", "func(text:Text, pattern:Pattern, replacement:Text, backref=$/\\/, recursive=yes -> Text)"}, {"replace_all", "Text$replace_all", "func(text:Text, replacements:{Pattern,Text}, backref=$/\\/, recursive=yes -> Text)"}, {"reversed", "Text$reversed", "func(text:Text -> Text)"}, + {"right_pad", "Text$right_pad", "func(text:Text, count:Int, pad=\" \" -> Text)"}, {"slice", "Text$slice", "func(text:Text, from=1, to=-1 -> Text)"}, {"split", "Text$split", "func(text:Text, pattern=$Pattern'' -> [Text])"}, {"starts_with", "Text$starts_with", "func(text,prefix:Text -> Bool)"}, diff --git a/stdlib/text.c b/stdlib/text.c index c870073..adbac3f 100644 --- a/stdlib/text.c +++ b/stdlib/text.c @@ -512,6 +512,47 @@ public Text_t Text$repeat(Text_t text, Int_t count) return ret; } +static Text_t Text$repeat_to_length(Text_t to_repeat, int64_t length) +{ + if (length <= 0) + return EMPTY_TEXT; + + Text_t repeated = EMPTY_TEXT; + while (repeated.length + to_repeat.length <= length) + repeated = concat2(repeated, to_repeat); + + if (repeated.length < length) + repeated = concat2(repeated, Text$slice(to_repeat, I_small(1), I(length - repeated.length))); + + assert(repeated.length == length); + return repeated; +} + +public Text_t Text$left_pad(Text_t text, Int_t count, Text_t padding) +{ + if (padding.length == 0) + fail("Cannot pad with an empty text!"); + + return concat2(Text$repeat_to_length(padding, Int64$from_int(count, false) - text.length), text); +} + +public Text_t Text$right_pad(Text_t text, Int_t count, Text_t padding) +{ + if (padding.length == 0) + fail("Cannot pad with an empty text!"); + + return concat2(text, Text$repeat_to_length(padding, Int64$from_int(count, false) - text.length)); +} + +public Text_t Text$middle_pad(Text_t text, Int_t count, Text_t padding) +{ + if (padding.length == 0) + fail("Cannot pad with an empty text!"); + + int64_t needed = Int64$from_int(count, false) - text.length; + return Texts(Text$repeat_to_length(padding, needed/2), text, Text$repeat_to_length(padding, (needed+1)/2)); +} + public Text_t Text$slice(Text_t text, Int_t first_int, Int_t last_int) { int64_t first = Int64$from_int(first_int, false); diff --git a/stdlib/text.h b/stdlib/text.h index d3aba3f..9923403 100644 --- a/stdlib/text.h +++ b/stdlib/text.h @@ -68,6 +68,9 @@ Array_t Text$lines(Text_t text); Closure_t Text$by_line(Text_t text); Text_t Text$join(Text_t glue, Array_t pieces); Text_t Text$repeat(Text_t text, Int_t count); +Text_t Text$left_pad(Text_t text, Int_t count, Text_t padding); +Text_t Text$right_pad(Text_t text, Int_t count, Text_t padding); +Text_t Text$middle_pad(Text_t text, Int_t count, Text_t padding); int32_t Text$get_grapheme_fast(TextIter_t *state, int64_t index); uint32_t Text$get_main_grapheme_fast(TextIter_t *state, int64_t index); void Text$serialize(const void *obj, FILE *out, Table_t *, const TypeInfo_t *); diff --git a/test/text.tm b/test/text.tm index a1b36d9..91edddb 100644 --- a/test/text.tm +++ b/test/text.tm @@ -361,3 +361,16 @@ func main(): = 1 >> concat4 == final = yes + + >> "x":left_pad(5) + = " x" + >> "x":right_pad(5) + = "x " + >> "x":middle_pad(5) + = " x " + >> "1234":left_pad(8, "XYZ") + = "XYZX1234" : Text + >> "1234":right_pad(8, "XYZ") + = "1234XYZX" : Text + >> "1234":middle_pad(9, "XYZ") + = "XY1234XYZ" : Text