feat: implement non-boolean format specifiers (#22)#82
Conversation
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
There was a problem hiding this comment.
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.
| result = Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString("C", culture); | ||
| return true; |
There was a problem hiding this comment.
: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.
| result = Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString("C", culture); | |
| return true; | |
| if (value is IFormattable currencyFormattable) | |
| { | |
| result = currencyFormattable.ToString("C", culture); | |
| return true; | |
| } |
| /// 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; |
There was a problem hiding this comment.
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.
| /// 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; | |
| } |
| <ComboBox ItemsSource="{Binding AvailableCultures}" | ||
| SelectedItem="{Binding SelectedCulture}" | ||
| Width="160" | ||
| ToolTip.Tip="Culture used for number and currency formatting"/> |
There was a problem hiding this comment.
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.
| ToolTip.Tip="Culture used for number and currency formatting"/> | |
| ToolTip.Tip="Culture used for number, currency, date formatting, string casing, and localized boolean values"/> |
- Use IFormattable for currency formatting instead of Convert.ToDecimal - Update culture selector tooltip to reflect all affected formatting
Summary
Implements all non-boolean format specifiers from #22, closing the documentation-vs-implementation gap:
:uppercase,:lowercase— culture-aware case transformation:currency(locale-aware),:number:FORMAT(any .NET numeric format string):date:FORMAT— any .NET date format string, supports DateTime, DateTimeOffset, and parseable date stringsnumber:N2,date:MMMM d, yyyy)Also closes #16 as duplicate.
Test plan
dotnet format --verify-no-changesclean