Skip to content

feat: implement non-boolean format specifiers (#22)#82

Merged
vaceslav merged 4 commits intomainfrom
feature/22-non-boolean-format-specifiers
Mar 6, 2026
Merged

feat: implement non-boolean format specifiers (#22)#82
vaceslav merged 4 commits intomainfrom
feature/22-non-boolean-format-specifiers

Conversation

@vaceslav
Copy link
Contributor

@vaceslav vaceslav commented Mar 6, 2026

Summary

Implements all non-boolean format specifiers from #22, closing the documentation-vs-implementation gap:

  • String formatters: :uppercase, :lowercase — culture-aware case transformation
  • Number formatters: :currency (locale-aware), :number:FORMAT (any .NET numeric format string)
  • Date formatters: :date:FORMAT — any .NET date format string, supports DateTime, DateTimeOffset, and parseable date strings
  • GUI: Culture selector dropdown (Invariant, en-US, de-DE, fr-FR, es-ES)
  • Regex: Updated to support compound specifiers (number:N2, date:MMMM d, yyyy)

Also closes #16 as duplicate.

Test plan

  • 1045 tests passing (31 new tests added)
  • dotnet format --verify-no-changes clean
  • Unit tests: PlaceholderFinder compound format parsing
  • Unit tests: ValueConverter for all formatter types (string, number, currency, date)
  • Integration tests: end-to-end Word document processing for each formatter
  • Edge cases: null values, non-matching types, invalid formats, culture variants
  • Manual: run GUI app, verify culture dropdown works with template processing

vaceslav added 3 commits March 6, 2026 08:52
Add :currency and :number:FORMAT format specifiers for numeric values.
- Update PlaceholderFinder regex to support compound specifiers (e.g., number:N2)
- Add number formatting logic to ValueConverter (currency, IFormattable)
- Add culture selector dropdown to GUI app
- Update documentation: README, CLAUDE.md, CHANGELOG, format-specifiers guide
Add :uppercase/:lowercase string formatters and 📅FORMAT date formatter.
- String formatters: culture-aware ToUpper/ToLower for string values
- Date formatter: supports DateTime, DateTimeOffset, and parseable date strings
- Update regex to allow non-word characters in compound format (e.g., date:MMMM d, yyyy)
- Update documentation: README, CLAUDE.md, CHANGELOG, format-specifiers guide
- Use DateTimeOffset for date formatting to preserve timezone info
- Parse date strings with InvariantCulture first for reliable ISO support
- Use InvariantCulture for numeric conversion in currency formatting
- Escape trailing } in placeholder regex for consistency
- Simplify CultureOption to a record, use range syntax, catch OverflowException
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR closes the documented-vs-implemented gap for non-boolean placeholder format specifiers by adding culture-aware string casing, numeric/currency formatting, and date formatting, plus updating parsing, docs, tests, and the GUI to select culture.

Changes:

  • Add non-boolean formatting support in placeholder value conversion (:uppercase, :lowercase, :currency, :number:FORMAT, :date:FORMAT).
  • Extend placeholder parsing to support compound specifiers (e.g., number:N2, date:MMMM d, yyyy).
  • Update docs/examples/changelog and add unit + integration test coverage; add GUI culture selector wired through the service layer.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/for-template-authors/format-specifiers.md Documents new string/number/date specifiers and examples.
TriasDev.Templify/Placeholders/ValueConverter.cs Implements formatting logic for string/number/date specifiers.
TriasDev.Templify/Placeholders/PlaceholderFinder.cs Updates placeholder regex to capture compound format specifiers.
TriasDev.Templify/Examples.md Updates guidance to recommend :currency / :number:* for formatting.
TriasDev.Templify.Tests/ValueConverterTests.cs Adds unit tests for new conversion/formatting behaviors.
TriasDev.Templify.Tests/PlaceholderFinderTests.cs Adds tests ensuring compound specifiers are parsed correctly.
TriasDev.Templify.Tests/Integration/FormatSpecifierIntegrationTests.cs Adds end-to-end Word processing tests for new formats.
TriasDev.Templify.Gui/Views/MainWindow.axaml Adds a culture dropdown to the UI.
TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs Adds culture options + passes selected culture to service calls.
TriasDev.Templify.Gui/Services/TemplifyService.cs Threads culture into PlaceholderReplacementOptions.
TriasDev.Templify.Gui/Services/ITemplifyService.cs Extends service API to accept optional culture.
TriasDev.Templify.Gui/Models/CultureOption.cs Introduces a display-friendly culture option model for the dropdown.
README.md Adds quick documentation for the new format specifiers and culture usage.
CLAUDE.md Updates syntax examples to include new specifiers.
CHANGELOG.md Records the new formatting features and GUI culture selector under Unreleased.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +103
result = Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString("C", culture);
return true;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

:currency formatting converts the value to decimal before applying the "C" format. This makes :currency behave differently from :number:C (which formats via IFormattable) and can cause :currency to silently fall back for values that don’t fit in decimal (e.g., large double/long). Consider formatting currency via IFormattable.ToString("C", culture) for all numeric types to keep behavior consistent and avoid unnecessary conversions/overflows.

Suggested change
result = Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString("C", culture);
return true;
if (value is IFormattable currencyFormattable)
{
result = currencyFormattable.ToString("C", culture);
return true;
}

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +169
/// Tries to parse a date string, first with InvariantCulture (for ISO formats),
/// then with the specified culture as a fallback.
/// </summary>
private static bool TryParseDateTime(string s, CultureInfo culture, out DateTimeOffset parsed)
{
// Try InvariantCulture first for reliable ISO date parsing (e.g., "2024-01-15", "2024-01-15T10:30:00+02:00")
if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
return true;
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

TryParseDateTime claims it tries InvariantCulture first “for reliable ISO date parsing”, but DateTimeOffset.TryParse with InvariantCulture will successfully parse many non-ISO/locale-style date strings too. This can lead to culture-ambiguous strings being interpreted using invariant rules even when a specific culture was provided. Consider switching to TryParseExact for a small set of ISO/roundtrip patterns first, and only then falling back to TryParse with the provided culture.

Suggested change
/// Tries to parse a date string, first with InvariantCulture (for ISO formats),
/// then with the specified culture as a fallback.
/// </summary>
private static bool TryParseDateTime(string s, CultureInfo culture, out DateTimeOffset parsed)
{
// Try InvariantCulture first for reliable ISO date parsing (e.g., "2024-01-15", "2024-01-15T10:30:00+02:00")
if (DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
return true;
/// Tries to parse a date string, first with InvariantCulture using ISO/roundtrip formats,
/// then with the specified culture as a fallback.
/// </summary>
private static bool TryParseDateTime(string s, CultureInfo culture, out DateTimeOffset parsed)
{
// Try InvariantCulture first for reliable ISO date parsing (e.g., "2024-01-15", "2024-01-15T10:30:00+02:00")
// Use exact formats to avoid ambiguously interpreting culture-specific date strings.
string[] isoFormats =
{
"o", // Round-trip pattern with offset
"O", // Round-trip pattern (alias)
"yyyy-MM-dd", // Date only
"yyyy-MM-ddTHH:mm:ssK", // Date and time with kind/offset
"yyyy-MM-ddTHH:mm:sszzz" // Date and time with offset
};
foreach (var isoFormat in isoFormats)
{
if (DateTimeOffset.TryParseExact(s, isoFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
return true;
}

Copilot uses AI. Check for mistakes.
<ComboBox ItemsSource="{Binding AvailableCultures}"
SelectedItem="{Binding SelectedCulture}"
Width="160"
ToolTip.Tip="Culture used for number and currency formatting"/>
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The culture selector tooltip says it’s used for “number and currency formatting”, but the selected culture is also applied to date formatting (:date:...), culture-aware string casing (:uppercase/:lowercase), and default boolean formatter localization. Updating the tooltip text would avoid misleading users about what will change when they switch cultures.

Suggested change
ToolTip.Tip="Culture used for number and currency formatting"/>
ToolTip.Tip="Culture used for number, currency, date formatting, string casing, and localized boolean values"/>

Copilot uses AI. Check for mistakes.
- Use IFormattable for currency formatting instead of Convert.ToDecimal
- Update culture selector tooltip to reflect all affected formatting
@vaceslav vaceslav merged commit a4a633c into main Mar 6, 2026
14 checks passed
@vaceslav vaceslav deleted the feature/22-non-boolean-format-specifiers branch March 6, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add custom value formatters for dates, numbers, and currency

2 participants