diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java index 357ec548b..3fd0dfd24 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.Locale; +import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.function.Consumer; @@ -216,5 +217,21 @@ default boolean isFalse() { return OptionalDouble.empty(); } } + + /** + * Try to return this argument as a {@code char}. + * + *

The optional will only be present if the value is a valid char.

+ * + * @return an optional providing the value of this argument as an integer + * @since 4.10.0 + */ + default @NotNull Optional asChar() { + if (this.value().length() == 1) { + return Optional.of(this.value().charAt(0)); + } else { + return Optional.empty(); + } + } } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/Formatter.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/Formatter.java new file mode 100644 index 000000000..d835f3b31 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/Formatter.java @@ -0,0 +1,100 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2022 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.tag.resolver; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; +import net.kyori.adventure.text.minimessage.tag.Tag; +import org.jetbrains.annotations.NotNull; + +/** + * Tag resolvers producing tags that insert formatted values. + * + *

These are effectively placeholders.

+ * + * @since 4.10.0 + */ +public final class Formatter { + private Formatter() { + } + + /** + * Creates a replacement that inserts a number as a component. The component will be formatted by the provided DecimalFormat. + * + *

This tag expects a format as attribute. Refer to {@link DecimalFormat} for usable patterns.

+ * + *

You can specify the decimal and grouping separator by adding those as another attribute.

+ * + *

This replacement is auto-closing, so its style will not influence the style of following components.

+ * + * @param key the key + * @param number the number + * @return the placeholder + * @since 4.10.0 + */ + public static TagResolver formatNumber(final @NotNull String key, final Number number) { + return TagResolver.resolver(key, (argumentQueue, context) -> { + final NumberFormat decimalFormat; + if (argumentQueue.hasNext()) { + final String locale = argumentQueue.pop().value(); + if (argumentQueue.hasNext()) { + final String format = argumentQueue.pop().value(); + decimalFormat = new DecimalFormat(format, new DecimalFormatSymbols(Locale.forLanguageTag(locale))); + } else { + if (locale.contains(".")) { + decimalFormat = new DecimalFormat(locale, DecimalFormatSymbols.getInstance()); + } else { + decimalFormat = DecimalFormat.getInstance(Locale.forLanguageTag(locale)); + } + } + } else { + decimalFormat = DecimalFormat.getInstance(); + } + return Tag.inserting(context.deserialize(decimalFormat.format(number))); + }); + } + + /** + * Creates a replacement that inserts a date or a time as a component. The component will be formatted by the provided Date Format. + * + *

This tag expects a format as attribute. Refer to {@link DateTimeFormatter} for usable patterns.

+ * + *

This replacement is auto-closing, so its style will not influence the style of following components.

+ * + * @param key the key + * @param time the time + * @return the placeholder + * @since 4.10.0 + */ + public static TagResolver formatDate(final @NotNull String key, final TemporalAccessor time) { + return TagResolver.resolver(key, (argumentQueue, context) -> { + final String format = argumentQueue.popOr("Format expected.").value(); + return Tag.inserting(context.deserialize(DateTimeFormatter.ofPattern(format).format(time))); + }); + } +} diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/FormatterTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/FormatterTest.java new file mode 100644 index 000000000..a81b835eb --- /dev/null +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/FormatterTest.java @@ -0,0 +1,103 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2022 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.tag; + +import java.time.LocalDateTime; +import java.time.Month; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.AbstractTest; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.minimessage.tag.resolver.Formatter.formatDate; +import static net.kyori.adventure.text.minimessage.tag.resolver.Formatter.formatNumber; + +public class FormatterTest extends AbstractTest { + @Test + void testNumberFormatter() { + final String input = " is a nice number"; + final Component expected = text("20.00 is a nice number"); + + this.assertParsedEquals( + expected, + input, + formatNumber("mynumber", 20d) + ); + } + + @Test + void testNumberFormatterLang() { + final String input = " is a nice number"; + final Component expected = text("20,00 is a nice number"); + + this.assertParsedEquals( + expected, + input, + formatNumber("mynumber", 20d) + ); + } + + @Test + void testNumberFormatterWithDecimal() { + final String input = " is a nice number"; + final Component expected = text("2.000,00 is a nice number"); + + this.assertParsedEquals( + expected, + input, + formatNumber("double", 2000d) + ); + } + + @Test + void testNumberFormatterNegative() { + final String input = "#.00;-#.00'> is a nice number"; + final Component expectedNegative = text("-5.00", NamedTextColor.RED).append(text(" is a nice number", NamedTextColor.BLUE)); + final Component expectedPositive = text("5.00", NamedTextColor.GREEN).append(text(" is a nice number", NamedTextColor.BLUE)); + + this.assertParsedEquals( + expectedNegative, + input, + formatNumber("double", -5) + ); + this.assertParsedEquals( + expectedPositive, + input, + formatNumber("double", 5) + ); + } + + @Test + void testDateFormatter() { + final String input = " is a date"; + final Component expected = text("2022-02-26 21:00:00 is a date"); + + this.assertParsedEquals( + expected, + input, + formatDate("date", LocalDateTime.of(2022, Month.FEBRUARY, 26, 21, 0, 0)) + ); + } +}