From 54b28910acb13a5f7b86bb488088ffcb64e916f9 Mon Sep 17 00:00:00 2001 From: Pr0metheus2 Date: Wed, 26 Aug 2026 12:07:41 +0200 Subject: [PATCH 1/6] Fix/MultiFile pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed/MultiFile rollover patterns with an index before the extension: - RolloverFilenameBuilder: the parser was dropping literal text after the final placeholder, including .log. - *$J(.).log now correctly expands: app.log → app.1.log → app.2.log. - Updated MultiFile mask dialog legend: - Fixed incorrectly displayed and placeholders in English and German resources. - Added both usage examples: *$J(.) → app.log, app.log.1, app.log.2 *$J(.).log → app.log, app.1.log, app.2.log - Added the examples in English, German, and Chinese. - Enlarged the dialog and syntax-help label so the date-format lines are no longer clipped. - Fixed/Items in menu File/Multifile -> "Enable MultiFile" and "Multi file mask...", were always greyed out. Now they are displayed normally after the .log file is open. - removed the BackColor and ForeColor assignments for both: - multiFileEnabledStripMenuItem - multifileMaskToolStripMenuItem - Added regression coverage: - Filename-builder tests for app.1.log and app.2.log. - Reader-level test that loads app.log, app.1.log, and app.2.log in correct order using *$J(.).log. - Kept the existing *$J(.) coverage for app.log.1 / app.log.2. Verification: - Full test suite: 1064 passed, 0 failed, 7 skipped. - Resource project build: passed with 0 warnings, 0 errors. --- .../Classes/Log/RolloverFilenameBuilder.cs | 3 ++- src/LogExpert.Resources/Resources.de.resx | 10 ++++++--- src/LogExpert.Resources/Resources.resx | 10 ++++++--- src/LogExpert.Resources/Resources.zh-CN.resx | 4 ++++ src/LogExpert.Tests/LogExpert.Tests.csproj | 6 +++++ src/LogExpert.Tests/RollingNameTest.cs | 4 +++- .../LogfileReaderMultiFileFlagTests.cs | 22 +++++++++++++++++-- .../LogTabWindow/LogTabWindow.designer.cs | 8 +++---- .../Dialogs/MultiFileMaskDialog.Designer.cs | 12 +++++----- 9 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs b/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs index 3b8f1061..e449884b 100644 --- a/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs +++ b/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs @@ -238,9 +238,10 @@ private string EscapeNonvarRegions (string formatString) } } + _ = result.Append(Regex.Escape(segment.ToString())); fmt = result.ToString().Replace('\xFFFD', '*'); return fmt; } #endregion -} \ No newline at end of file +} diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index e0e19567..3852c64c 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -1680,12 +1680,16 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools Muster syntax: * = alle Zeichen (wildcard) -$D(&lt;date&gt;) = Datumsmuster +$D(<date>) = Datumsmuster $I = Dateiindexnummer $J = Dateiindexnummer, versteckt wenn 09 -$J(&lt;prefix&gt;) = Wie $J, jedoch wird ein &lt;prefix&gt; hinzugefügt when es nicht 0 ist +$J(<prefix>) = Wie $J, jedoch wird ein <prefix> hinzugefügt when es nicht 0 ist -&lt;date&gt;: +Beispiele: +*$J(.) → app.log, app.log.1, app.log.2 +*$J(.).log → app.log, app.1.log, app.2.log + +<date>: DD = Tag MM = Monat YY[YY] = Jahr diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index a08b755c..70d5e60d 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -1743,12 +1743,16 @@ Checked tools will appear in the icon bar. All other tools are available in the Pattern syntax: * = any characters (wildcard) -$D(&lt;date&gt;) = Date pattern +$D(<date>) = Date pattern $I = File index number $J = File index number, hidden when zero -$J(&lt;prefix&gt;) = Like $J, but adding &lt;prefix&gt; when non-zero +$J(<prefix>) = Like $J, but adding <prefix> when non-zero -&lt;date&gt;: +Examples: +*$J(.) → app.log, app.log.1, app.log.2 +*$J(.).log → app.log, app.1.log, app.2.log + +<date>: DD = day MM = month YY[YY] = year diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx index f4af56f5..826db482 100644 --- a/src/LogExpert.Resources/Resources.zh-CN.resx +++ b/src/LogExpert.Resources/Resources.zh-CN.resx @@ -1554,6 +1554,10 @@ $I = 文件索引编号 $J = 文件索引编号,为零时隐藏 $J(<前缀>) = 类似 $J,但在非零时添加 <前缀> +示例: +*$J(.) → app.log, app.log.1, app.log.2 +*$J(.).log → app.log, app.1.log, app.2.log + <日期>: DD = 日 MM = 月 diff --git a/src/LogExpert.Tests/LogExpert.Tests.csproj b/src/LogExpert.Tests/LogExpert.Tests.csproj index addb6a7a..2ead6531 100644 --- a/src/LogExpert.Tests/LogExpert.Tests.csproj +++ b/src/LogExpert.Tests/LogExpert.Tests.csproj @@ -112,6 +112,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/src/LogExpert.Tests/RollingNameTest.cs b/src/LogExpert.Tests/RollingNameTest.cs index a85f94b4..8f0a079c 100644 --- a/src/LogExpert.Tests/RollingNameTest.cs +++ b/src/LogExpert.Tests/RollingNameTest.cs @@ -32,6 +32,7 @@ public void TestFilename1(string expectedResult, string formatString) [TestCase("engine.log", "engine1.log","engine$J.log")] [TestCase("engine1.log", "engine2.log","engine$J.log")] [TestCase("engine.log", "engine.log.1","*$J(.)")] + [TestCase("engine.log", "engine.1.log", "*$J(.).log")] [TestCase("engine_2010-06-12.log", "engine_2010-06-12.log.1", "*$D(yyyy-MM-dd).log$J(.)")] public void TestFilenameAnd1(string fileName, string expectedResult, string formatString) { @@ -45,6 +46,7 @@ public void TestFilenameAnd1(string fileName, string expectedResult, string form [Test] [TestCase("engine.log", "engine.log.2","*$J(.)")] [TestCase("engine.log", "engine.log.2","*.log$J(.)")] + [TestCase("engine.log", "engine.2.log", "*$J(.).log")] public void TestFilenameAnd2(string fileName, string expectedResult, string formatString) { RolloverFilenameBuilder fnb = new(formatString); @@ -65,4 +67,4 @@ public void TestFilenameMinus1(string fileName, string expectedResult, string fo var name = fnb.BuildFileName(); Assert.That(name, Is.EqualTo("engine.log")); } -} \ No newline at end of file +} diff --git a/src/LogExpert.Tests/StreamReaderTests/LogfileReaderMultiFileFlagTests.cs b/src/LogExpert.Tests/StreamReaderTests/LogfileReaderMultiFileFlagTests.cs index 5522313d..ab3edd74 100644 --- a/src/LogExpert.Tests/StreamReaderTests/LogfileReaderMultiFileFlagTests.cs +++ b/src/LogExpert.Tests/StreamReaderTests/LogfileReaderMultiFileFlagTests.cs @@ -26,6 +26,8 @@ public void SetUp () File.Copy(Path.Combine(_testDataDirectory, "app.log"), _logFile); File.Copy(Path.Combine(_testDataDirectory, "app.log.1"), _logFile + ".1"); + File.Copy(Path.Combine(_testDataDirectory, "app.1.log"), Path.Combine(_testDirectory, "app.1.log")); + File.Copy(Path.Combine(_testDataDirectory, "app.2.log"), Path.Combine(_testDirectory, "app.2.log")); _ = PluginRegistry.PluginRegistry.Create(_testDirectory, 500); } @@ -68,6 +70,22 @@ public void SingleFileCtor_MultiFileTrue_ExpandsRollover () }); } + [Test] + public void SingleFileCtor_MultiFileTrue_LoadsIndexBeforeExtension () + { + var options = new MultiFileOptions { FormatPattern = "*$J(.).log" }; + using var reader = CreateSingleFileReader(multiFile: true, options); + + reader.ReadFiles(); + + Assert.Multiple(() => + { + Assert.That(reader.IsMultiFile, Is.True); + Assert.That(reader.GetLogFileInfoList().Select(file => Path.GetFileName(file.FullName)), + Is.EqualTo(new[] { "app.2.log", "app.1.log", "app.log" })); + }); + } + [Test] public void MultiFileCtor_AlwaysMultiFile () { @@ -91,7 +109,7 @@ public void MultiFileCtor_AlwaysMultiFile () }); } - private LogfileReader CreateSingleFileReader (bool multiFile) + private LogfileReader CreateSingleFileReader (bool multiFile, MultiFileOptions? options = null) { return new LogfileReader( _logFile, @@ -99,7 +117,7 @@ private LogfileReader CreateSingleFileReader (bool multiFile) multiFile, bufferCount: 40, linesPerBuffer: 50, - new MultiFileOptions(), + options ?? new MultiFileOptions(), ReaderType.System, PluginRegistry.PluginRegistry.Instance, maximumLineLength: 500, diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs index c7d64109..add47f8c 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs @@ -269,9 +269,9 @@ private void InitializeComponent () // // multiFileEnabledStripMenuItem // - multiFileEnabledStripMenuItem.BackColor = SystemColors.Control; + // multiFileEnabledStripMenuItem.BackColor = SystemColors.Control; multiFileEnabledStripMenuItem.CheckOnClick = true; - multiFileEnabledStripMenuItem.ForeColor = SystemColors.ControlDarkDark; + // multiFileEnabledStripMenuItem.ForeColor = SystemColors.ControlDarkDark; multiFileEnabledStripMenuItem.Name = "multiFileEnabledStripMenuItem"; multiFileEnabledStripMenuItem.Size = new Size(165, 22); multiFileEnabledStripMenuItem.Text = "Enable MultiFile"; @@ -279,8 +279,8 @@ private void InitializeComponent () // // multifileMaskToolStripMenuItem // - multifileMaskToolStripMenuItem.BackColor = SystemColors.Control; - multifileMaskToolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; + // multifileMaskToolStripMenuItem.BackColor = SystemColors.Control; + // multifileMaskToolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; multifileMaskToolStripMenuItem.Name = "multifileMaskToolStripMenuItem"; multifileMaskToolStripMenuItem.Size = new Size(165, 22); multifileMaskToolStripMenuItem.Text = "File name mask..."; diff --git a/src/LogExpert.UI/Dialogs/MultiFileMaskDialog.Designer.cs b/src/LogExpert.UI/Dialogs/MultiFileMaskDialog.Designer.cs index d57d7e64..18763c3b 100644 --- a/src/LogExpert.UI/Dialogs/MultiFileMaskDialog.Designer.cs +++ b/src/LogExpert.UI/Dialogs/MultiFileMaskDialog.Designer.cs @@ -121,7 +121,7 @@ private void InitializeComponent() // this.syntaxHelpLabel.Location = new System.Drawing.Point(15, 140); this.syntaxHelpLabel.Name = "syntaxHelpLabel"; - this.syntaxHelpLabel.Size = new System.Drawing.Size(402, 194); + this.syntaxHelpLabel.Size = new System.Drawing.Size(402, 254); this.syntaxHelpLabel.TabIndex = 7; this.syntaxHelpLabel.Text = "Syntax Help Label"; // @@ -129,7 +129,7 @@ private void InitializeComponent() // this.buttonOk.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.buttonOk.DialogResult = System.Windows.Forms.DialogResult.OK; - this.buttonOk.Location = new System.Drawing.Point(261, 347); + this.buttonOk.Location = new System.Drawing.Point(261, 407); this.buttonOk.Name = "buttonOk"; this.buttonOk.Size = new System.Drawing.Size(75, 23); this.buttonOk.TabIndex = 8; @@ -141,7 +141,7 @@ private void InitializeComponent() // this.buttonCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.buttonCancel.Location = new System.Drawing.Point(342, 347); + this.buttonCancel.Location = new System.Drawing.Point(342, 407); this.buttonCancel.Name = "buttonCancel"; this.buttonCancel.Size = new System.Drawing.Size(75, 23); this.buttonCancel.TabIndex = 9; @@ -152,7 +152,7 @@ private void InitializeComponent() // this.AcceptButton = this.buttonOk; this.CancelButton = this.buttonCancel; - this.ClientSize = new System.Drawing.Size(434, 386); + this.ClientSize = new System.Drawing.Size(434, 446); this.Controls.Add(this.buttonCancel); this.Controls.Add(this.buttonOk); this.Controls.Add(this.syntaxHelpLabel); @@ -162,7 +162,7 @@ private void InitializeComponent() this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MaximizeBox = false; this.MinimizeBox = false; - this.MinimumSize = new System.Drawing.Size(329, 420); + this.MinimumSize = new System.Drawing.Size(329, 480); this.Name = "MultiFileMaskDialog"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "MultiFile settings"; @@ -188,4 +188,4 @@ private void InitializeComponent() private System.Windows.Forms.Label syntaxHelpLabel; private System.Windows.Forms.Button buttonOk; private System.Windows.Forms.Button buttonCancel; - } \ No newline at end of file + } From 3989ae2a5cfc7e7a54ccfaa0da3cbd9c105948dd Mon Sep 17 00:00:00 2001 From: Pr0metheus2 Date: Wed, 26 Aug 2026 13:22:16 +0200 Subject: [PATCH 2/6] test: add multi-file index test data --- src/LogExpert.Tests/TestData/app.1.log | 1 + src/LogExpert.Tests/TestData/app.2.log | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/LogExpert.Tests/TestData/app.1.log create mode 100644 src/LogExpert.Tests/TestData/app.2.log diff --git a/src/LogExpert.Tests/TestData/app.1.log b/src/LogExpert.Tests/TestData/app.1.log new file mode 100644 index 00000000..dc4e726a --- /dev/null +++ b/src/LogExpert.Tests/TestData/app.1.log @@ -0,0 +1 @@ +app.1.log \ No newline at end of file diff --git a/src/LogExpert.Tests/TestData/app.2.log b/src/LogExpert.Tests/TestData/app.2.log new file mode 100644 index 00000000..67dfe9df --- /dev/null +++ b/src/LogExpert.Tests/TestData/app.2.log @@ -0,0 +1 @@ +app.2.log \ No newline at end of file From 15c3d960eb1dd8f3e11729998a72f5da9aa2b81e Mon Sep 17 00:00:00 2001 From: Pr0metheus Date: Sat, 29 Aug 2026 11:36:47 +0200 Subject: [PATCH 3/6] Fixed from when to wenn --- src/LogExpert.Resources/Resources.de.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index 3852c64c..ac74e3ad 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -1683,7 +1683,7 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools $D(<date>) = Datumsmuster $I = Dateiindexnummer $J = Dateiindexnummer, versteckt wenn 09 -$J(<prefix>) = Wie $J, jedoch wird ein <prefix> hinzugefügt when es nicht 0 ist +$J(<prefix>) = Wie $J, jedoch wird ein <prefix> hinzugefügt wenn es nicht 0 ist Beispiele: *$J(.) → app.log, app.log.1, app.log.2 From 905c979e1a023e58d364109bc2f7843a78b0d9f5 Mon Sep 17 00:00:00 2001 From: Pr0metheus Date: Sat, 29 Aug 2026 11:37:14 +0200 Subject: [PATCH 4/6] Removed old commented lines --- .../Dialogs/LogTabWindow/LogTabWindow.designer.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs index add47f8c..9da5eea6 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.designer.cs @@ -269,9 +269,7 @@ private void InitializeComponent () // // multiFileEnabledStripMenuItem // - // multiFileEnabledStripMenuItem.BackColor = SystemColors.Control; multiFileEnabledStripMenuItem.CheckOnClick = true; - // multiFileEnabledStripMenuItem.ForeColor = SystemColors.ControlDarkDark; multiFileEnabledStripMenuItem.Name = "multiFileEnabledStripMenuItem"; multiFileEnabledStripMenuItem.Size = new Size(165, 22); multiFileEnabledStripMenuItem.Text = "Enable MultiFile"; @@ -279,8 +277,6 @@ private void InitializeComponent () // // multifileMaskToolStripMenuItem // - // multifileMaskToolStripMenuItem.BackColor = SystemColors.Control; - // multifileMaskToolStripMenuItem.ForeColor = SystemColors.ControlDarkDark; multifileMaskToolStripMenuItem.Name = "multifileMaskToolStripMenuItem"; multifileMaskToolStripMenuItem.Size = new Size(165, 22); multifileMaskToolStripMenuItem.Text = "File name mask..."; From 1fcb26251a698cedcdcdb562bd213488886dc59a Mon Sep 17 00:00:00 2001 From: Pr0metheus Date: Sat, 29 Aug 2026 12:01:31 +0200 Subject: [PATCH 5/6] Update RollingNameTest.cs Fixed by locating and parsing $D(...) in the escaped format string. Offsets remain correct when literal regex metacharacters appear before the date placeholder. Added a regression test for app.$D(yyyy-MM-dd).log that verifies date parsing and rebuilding. Added cases which cover: - $I with a trailing literal .log, starting from a rotated file. - $D plus $J(.) followed by a trailing .log, also starting from a rotated file. --- src/LogExpert.Tests/RollingNameTest.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/LogExpert.Tests/RollingNameTest.cs b/src/LogExpert.Tests/RollingNameTest.cs index 8f0a079c..3f9984a4 100644 --- a/src/LogExpert.Tests/RollingNameTest.cs +++ b/src/LogExpert.Tests/RollingNameTest.cs @@ -33,6 +33,10 @@ public void TestFilename1(string expectedResult, string formatString) [TestCase("engine1.log", "engine2.log","engine$J.log")] [TestCase("engine.log", "engine.log.1","*$J(.)")] [TestCase("engine.log", "engine.1.log", "*$J(.).log")] + [TestCase("engine.log.1", "engine.log.2", "*$J(.)")] + [TestCase("engine.1.log", "engine.2.log", "*$J(.).log")] + [TestCase("engine1.log", "engine2.log", "*$I.log")] + [TestCase("engine_2010-06-12.1.log", "engine_2010-06-12.2.log", "*$D(yyyy-MM-dd)$J(.).log")] [TestCase("engine_2010-06-12.log", "engine_2010-06-12.log.1", "*$D(yyyy-MM-dd).log$J(.)")] public void TestFilenameAnd1(string fileName, string expectedResult, string formatString) { @@ -56,6 +60,16 @@ public void TestFilenameAnd2(string fileName, string expectedResult, string form Assert.That(name, Is.EqualTo(expectedResult)); } + [Test] + public void BuildFileName_DatePatternAfterRegexMetacharacter_IncrementsDate () + { + RolloverFilenameBuilder fnb = new("app.$D(yyyy-MM-dd).log"); + fnb.SetFileName("app.2010-06-12.log"); + + fnb.IncrementDate(); + + Assert.That(fnb.BuildFileName(), Is.EqualTo("app.2010-06-13.log")); + } [Test] [TestCase("engine1.log", "engine.log","engine$J.log")] From 568bb043e5a42958450984067f3cfa4fde3152fa Mon Sep 17 00:00:00 2001 From: Pr0metheus Date: Sat, 29 Aug 2026 12:17:11 +0200 Subject: [PATCH 6/6] Update RolloverFilenameBuilder.cs - Added FlushEscaped() to share duplicated escaping code. - Fixed parsing of $J() from rotated files, so both: - app.log.1 - app.1.log correctly identify index 1. - Made wildcard matching non-greedy and anchored the generated regex to the complete filename. - Updated rebuilding logic to remove an existing prefix plus index together before inserting the new index. - Fixed $D(...) parsing to use positions in the escaped format string, preventing errors when a literal regex character such as . appears before $D. --- .../Classes/Log/RolloverFilenameBuilder.cs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs b/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs index e449884b..2d7b1476 100644 --- a/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs +++ b/src/LogExpert.Core/Classes/Log/RolloverFilenameBuilder.cs @@ -116,15 +116,23 @@ public string BuildFileName () if (_indexGroup != null && _indexGroup.Success) { - fileName = fileName.Remove(_indexGroup.Index, _indexGroup.Length); + var indexPosition = _indexGroup.Index; + var indexLength = _indexGroup.Length; + if (_condGroup != null && _condGroup.Success) + { + indexPosition = _condGroup.Index; + indexLength += _condGroup.Length; + } + + fileName = fileName.Remove(indexPosition, indexLength); if (!_hideZeroIndex || Index > 0) { var format = "D" + _indexGroup.Length; - fileName = fileName.Insert(_indexGroup.Index, Index.ToString(format)); + fileName = fileName.Insert(indexPosition, Index.ToString(format)); if (_hideZeroIndex && _condContent != null) { - fileName = fileName.Insert(_indexGroup.Index, _condContent); + fileName = fileName.Insert(indexPosition, _condContent); } } } @@ -141,14 +149,14 @@ public string BuildFileName () private void ParseFormatString (string formatString) { var fmt = EscapeNonvarRegions(formatString); - var datePos = formatString.IndexOf("$D(", StringComparison.Ordinal); + var datePos = fmt.IndexOf("$D(", StringComparison.Ordinal); if (datePos != -1) { - var endPos = formatString.IndexOf(')', datePos); + var endPos = fmt.IndexOf(')', datePos); if (endPos != -1) { - _dateTimeFormat = formatString.Substring(datePos + 3, endPos - datePos - 3) + _dateTimeFormat = fmt.Substring(datePos + 3, endPos - datePos - 3) .ToUpperInvariant() .Replace('D', 'd') .Replace('Y', 'y'); @@ -176,12 +184,15 @@ private void ParseFormatString (string formatString) } } - fmt = fmt.Replace("*", ".*", StringComparison.Ordinal); + fmt = fmt.Replace("*", ".*?", StringComparison.Ordinal); _hideZeroIndex = fmt.Contains("$J", StringComparison.Ordinal); fmt = fmt.Replace("$I", "(?'index'[\\d]+)", StringComparison.Ordinal); - fmt = fmt.Replace("$J", "(?'index'[\\d]*)", StringComparison.Ordinal); + var optionalIndexPattern = _condContent != null + ? $"(?:(?'cond'{Regex.Escape(_condContent)})(?'index'[\\d]+)|(?'index'))" + : "(?'index'[\\d]*)"; + fmt = fmt.Replace("$J", optionalIndexPattern, StringComparison.Ordinal); - _regex = new Regex(fmt); + _regex = new Regex(@"\A" + fmt + @"\z"); } private string EscapeNonvarRegions (string formatString) @@ -192,6 +203,12 @@ private string EscapeNonvarRegions (string formatString) StringBuilder result = new(); StringBuilder segment = new(); + void FlushEscaped () + { + _ = result.Append(Regex.Escape(segment.ToString())); + segment = new StringBuilder(); + } + for (var i = 0; i < fmt.Length; ++i) { switch (state) @@ -199,8 +216,7 @@ private string EscapeNonvarRegions (string formatString) case 0: // looking for $ if (fmt[i] == '$') { - _ = result.Append(Regex.Escape(segment.ToString())); - segment = new StringBuilder(); + FlushEscaped(); state = 1; } @@ -238,7 +254,7 @@ private string EscapeNonvarRegions (string formatString) } } - _ = result.Append(Regex.Escape(segment.ToString())); + FlushEscaped(); fmt = result.ToString().Replace('\xFFFD', '*'); return fmt; }