feat: implement filename parser with hyphen rules
This commit is contained in:
1 parent
bb040ef89b
commit
d6677b323b
3 files changed
+503
-1
No files matched your search
@@ -43,7 +43,8 @@
|
|||||||
|
|
||||||
- 仅认半角 `-`(全角 `-`、破折号 `—`/`–` 等不认)
|
- 仅认半角 `-`(全角 `-`、破折号 `—`/`–` 等不认)
|
||||||
- 无 `-`:歌手为空,整串 `Trim()` 为歌曲名
|
- 无 `-`:歌手为空,整串 `Trim()` 为歌曲名
|
||||||
- 有 `-`(多个时**以最后一个分割**):前段归歌手、后段归歌曲名(粗暴分配,交换按钮兜底),各自 `Trim()`
|
- 恰好一个 `-`:前段归歌手、后段归歌曲名(粗暴分配,交换按钮兜底),各自 `Trim()`
|
||||||
|
- 多个 `-`:歌手为空,步骤二输入的整串 `Trim()` 为歌曲名
|
||||||
|
|
||||||
**示例**:`A - B (Live - Tokyo)(修正版).mp3` → 歌手 `A`,歌曲名 `B`,注释 `Live - Tokyo 修正版`
|
**示例**:`A - B (Live - Tokyo)(修正版).mp3` → 歌手 `A`,歌曲名 `B`,注释 `Live - Tokyo 修正版`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
using SongTidy.Services;
|
||||||
|
|
||||||
|
namespace SongTidy.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains tests for <see cref="FilenameParser"/>.
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public sealed class FilenameParserTests {
|
||||||
|
#region Successful parsing
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the documented example with one English and one Chinese bracket group.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ParsesSpecificationExample() {
|
||||||
|
AssertParse("A - B (Live - Tokyo)(修正版)", "A", "B", "Live - Tokyo 修正版");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that multiple comment groups are joined with a single half-width space.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ParsesMultipleCommentGroups() {
|
||||||
|
AssertParse("A - B (Live)(Remix)", "A", "B", "Live Remix");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that Chinese bracket groups are extracted like English bracket groups.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ParsesChineseBracketComment() {
|
||||||
|
AssertParse("歌手 - 歌名(现场)", "歌手", "歌名", "现场");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that all comment groups are removed from the text used for artist and title.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RemovesAllCommentGroupsFromSource() {
|
||||||
|
AssertParse("A - B (Live) (Tokyo)", "A", "B", "Live Tokyo");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that nested brackets stay as literal text inside the comment.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void KeepsNestedBracketsInsideComment() {
|
||||||
|
AssertParse("A - B (Live (Tokyo) Mix)", "A", "B", "Live (Tokyo) Mix");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that nested full-width brackets stay as literal text inside an English comment.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void PreservesFullWidthNestedBrackets() {
|
||||||
|
AssertParse("A - B (Live(东京))", "A", "B", "Live(东京)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that only the outermost pair of a nested group is treated as the comment.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TreatsOnlyOutermostBracketGroupAsComment() {
|
||||||
|
AssertParse("A - B ((Live))", "A", "B", "(Live)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a comment can be extracted from a name without a hyphen.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ParsesCommentWithoutArtistSeparator() {
|
||||||
|
AssertParse("Song (Live)", string.Empty, "Song", "Live");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Invalid bracket structures
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an unmatched opening bracket disables comment extraction entirely.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForUnmatchedOpeningBracket() {
|
||||||
|
AssertParse("A - B (Live", "A", "B (Live", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an unmatched closing bracket disables comment extraction entirely.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForUnmatchedClosingBracket() {
|
||||||
|
AssertParse("A - B Live)", "A", "B Live)", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that opening and closing brackets of different types cannot pair.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForMismatchedBracketTypes() {
|
||||||
|
AssertParse("A - B (Live)", "A", "B (Live)", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an extra closing bracket after a valid pair disables extraction entirely.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForExtraClosingBracket() {
|
||||||
|
AssertParse("A - B (Live))", "A", "B (Live))", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an extra opening bracket after a valid pair disables extraction entirely.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForExtraOpeningBracket() {
|
||||||
|
AssertParse("A - B (Live) (", "A", "B (Live) (", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an unclosed outer bracket invalidates otherwise complete nested brackets.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForUnclosedOuterBracket() {
|
||||||
|
AssertParse("A - B (Live (Tokyo)", "A", "B (Live (Tokyo)", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that crossing bracket groups are treated as an invalid structure.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void AbandonsExtractionForCrossingBracketGroups() {
|
||||||
|
AssertParse("(a(b))", string.Empty, "(a(b))", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Hyphen handling
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a single half-width hyphen separates artist from title.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void SplitsSingleHalfWidthHyphen() {
|
||||||
|
AssertParse("A - B", "A", "B", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that multiple half-width hyphens make the whole remaining text the title.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TreatsMultipleHyphensAsTitleOnly() {
|
||||||
|
AssertParse("A - B - C", string.Empty, "A - B - C", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that hyphens inside a removed comment do not count when applying the multiple-hyphen rule.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void IgnoresHyphensInsideCommentsWhenSplitting() {
|
||||||
|
AssertParse("A - B (Live - Tokyo - Mix)", "A", "B", "Live - Tokyo - Mix");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a full-width hyphen is not treated as a separator.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void DoesNotSplitOnFullWidthHyphen() {
|
||||||
|
AssertParse("A - B", string.Empty, "A - B", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an em dash is not treated as a separator.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void DoesNotSplitOnEmDash() {
|
||||||
|
AssertParse("A — B", string.Empty, "A — B", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a name without a hyphen produces an empty artist and a full title.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ReturnsEmptyArtistWhenNoHyphenExists() {
|
||||||
|
AssertParse("Song Title", string.Empty, "Song Title", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an empty input produces three empty fields.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ReturnsEmptyFieldsForEmptyInput() {
|
||||||
|
AssertParse(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Whitespace handling
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that standard whitespace around the artist and title is removed.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TrimsStandardWhitespaceAroundArtistAndTitle() {
|
||||||
|
AssertParse(" A - B ", "A", "B", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that standard whitespace around extracted comment text is removed.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TrimsStandardWhitespaceInsideComments() {
|
||||||
|
AssertParse("A - B ( Live )", "A", "B", "Live");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that full-width spaces inside comment text are preserved.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void PreservesFullWidthSpacesInsideComments() {
|
||||||
|
AssertParse("A - B (\u3000Live\u3000)", "A", "B", "\u3000Live\u3000");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that full-width spaces around artist and title are preserved.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void PreservesFullWidthSpacesAroundFields() {
|
||||||
|
AssertParse("A\u3000-\u3000B", "A\u3000", "\u3000B", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Argument validation
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a null input is rejected.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ThrowsForNullInput() {
|
||||||
|
Assert.Throws<ArgumentNullException>(() => FilenameParser.Parse(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the input and asserts all three resulting fields.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The file name to parse.</param>
|
||||||
|
/// <param name="expectedArtist">The expected artist value.</param>
|
||||||
|
/// <param name="expectedTitle">The expected title value.</param>
|
||||||
|
/// <param name="expectedComment">The expected comment value.</param>
|
||||||
|
private static void AssertParse(
|
||||||
|
string input,
|
||||||
|
string expectedArtist,
|
||||||
|
string expectedTitle,
|
||||||
|
string expectedComment) {
|
||||||
|
FilenameParseResult result = FilenameParser.Parse(input);
|
||||||
|
|
||||||
|
Assert.AreEqual(expectedArtist, result.Artist, "Artist mismatch.");
|
||||||
|
Assert.AreEqual(expectedTitle, result.Title, "Title mismatch.");
|
||||||
|
Assert.AreEqual(expectedComment, result.Comment, "Comment mismatch.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SongTidy.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a file name into artist, title, and comment fields using plain string operations.
|
||||||
|
/// </summary>
|
||||||
|
public static class FilenameParser {
|
||||||
|
#region Public API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a file name that does not include its extension.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileNameWithoutExtension">The file name to parse.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The extracted artist, title, and comment. Missing fields are returned as empty strings.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// After comment extraction, a single half-width hyphen separates artist from title.
|
||||||
|
/// When no hyphen or more than one hyphen remains, the artist is empty and the whole text is the title.
|
||||||
|
/// </remarks>
|
||||||
|
public static FilenameParseResult Parse(string fileNameWithoutExtension) {
|
||||||
|
ArgumentNullException.ThrowIfNull(fileNameWithoutExtension);
|
||||||
|
|
||||||
|
string source = fileNameWithoutExtension;
|
||||||
|
string comment = string.Empty;
|
||||||
|
|
||||||
|
// Extract comments only when every bracket in the name is paired correctly.
|
||||||
|
if (TryExtractTopLevelPairs(fileNameWithoutExtension, out List<BracketPair> pairs)) {
|
||||||
|
List<string> commentParts = new(pairs.Count);
|
||||||
|
|
||||||
|
// Each outermost bracket pair contributes one trimmed comment fragment.
|
||||||
|
foreach (BracketPair pair in pairs) {
|
||||||
|
commentParts.Add(TrimStandardWhitespace(fileNameWithoutExtension[(pair.Start + 1)..pair.End]));
|
||||||
|
}
|
||||||
|
|
||||||
|
comment = string.Join(' ', commentParts);
|
||||||
|
source = RemoveRanges(fileNameWithoutExtension, pairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split only when exactly one half-width hyphen remains after comment extraction.
|
||||||
|
int hyphenIndex = source.IndexOf('-');
|
||||||
|
|
||||||
|
if (hyphenIndex < 0) {
|
||||||
|
return new FilenameParseResult(string.Empty, TrimStandardWhitespace(source), comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple hyphens make the whole remaining text the title.
|
||||||
|
if (hyphenIndex != source.LastIndexOf('-')) {
|
||||||
|
return new FilenameParseResult(string.Empty, TrimStandardWhitespace(source), comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
string artist = TrimStandardWhitespace(source[..hyphenIndex]);
|
||||||
|
string title = TrimStandardWhitespace(source[(hyphenIndex + 1)..]);
|
||||||
|
|
||||||
|
return new FilenameParseResult(artist, title, comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Bracket extraction
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds all outermost bracket pairs and verifies that the bracket structure is valid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The text to inspect.</param>
|
||||||
|
/// <param name="pairs">
|
||||||
|
/// Receives the outermost bracket pairs in source order. The list is empty when validation fails.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true"/> when every bracket is paired with a bracket of the same type;
|
||||||
|
/// otherwise, <see langword="false"/>.
|
||||||
|
/// </returns>
|
||||||
|
private static bool TryExtractTopLevelPairs(string source, out List<BracketPair> pairs) {
|
||||||
|
pairs = new List<BracketPair>();
|
||||||
|
Stack<OpenBracket> openBrackets = new();
|
||||||
|
|
||||||
|
for (int index = 0; index < source.Length; index++) {
|
||||||
|
char character = source[index];
|
||||||
|
|
||||||
|
// Record every opening bracket; its matching close is resolved by the stack.
|
||||||
|
if (IsOpeningBracket(character)) {
|
||||||
|
openBrackets.Push(new OpenBracket(index, character));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsClosingBracket(character)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A closing bracket without a same-type opening bracket invalidates the whole extraction.
|
||||||
|
if (openBrackets.Count == 0) {
|
||||||
|
pairs.Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenBracket opening = openBrackets.Pop();
|
||||||
|
|
||||||
|
if (!IsMatchingPair(opening.Character, character)) {
|
||||||
|
pairs.Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only outermost pairs; nested brackets remain literal text inside the comment.
|
||||||
|
if (openBrackets.Count == 0) {
|
||||||
|
pairs.Add(new BracketPair(opening.Index, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any opening bracket left on the stack has no matching close.
|
||||||
|
if (openBrackets.Count > 0) {
|
||||||
|
pairs.Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a character opens an English or Chinese bracket pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="character">The character to inspect.</param>
|
||||||
|
/// <returns><see langword="true"/> for an opening bracket.</returns>
|
||||||
|
private static bool IsOpeningBracket(char character) {
|
||||||
|
return character is '(' or '(';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a character closes an English or Chinese bracket pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="character">The character to inspect.</param>
|
||||||
|
/// <returns><see langword="true"/> for a closing bracket.</returns>
|
||||||
|
private static bool IsClosingBracket(char character) {
|
||||||
|
return character is ')' or ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether an opening bracket and a closing bracket belong to the same bracket type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opening">The opening bracket character.</param>
|
||||||
|
/// <param name="closing">The closing bracket character.</param>
|
||||||
|
/// <returns><see langword="true"/> when both characters form a valid pair.</returns>
|
||||||
|
private static bool IsMatchingPair(char opening, char closing) {
|
||||||
|
return (opening == '(' && closing == ')')
|
||||||
|
|| (opening == '(' && closing == ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a new string with the supplied ranges removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The text to copy.</param>
|
||||||
|
/// <param name="ranges">The non-overlapping ranges to omit, ordered by start position.</param>
|
||||||
|
/// <returns>The text that remains after all ranges are removed.</returns>
|
||||||
|
private static string RemoveRanges(string source, List<BracketPair> ranges) {
|
||||||
|
StringBuilder builder = new(source.Length);
|
||||||
|
int currentIndex = 0;
|
||||||
|
|
||||||
|
foreach (BracketPair range in ranges) {
|
||||||
|
if (range.Start > currentIndex) {
|
||||||
|
builder.Append(source, currentIndex, range.Start - currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = range.End + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < source.Length) {
|
||||||
|
builder.Append(source, currentIndex, source.Length - currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Text helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trims ASCII whitespace from both ends while preserving full-width spaces.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The text to trim.</param>
|
||||||
|
/// <returns>The trimmed text.</returns>
|
||||||
|
private static string TrimStandardWhitespace(string value) {
|
||||||
|
int start = 0;
|
||||||
|
int end = value.Length - 1;
|
||||||
|
|
||||||
|
// Move the start index past leading standard whitespace.
|
||||||
|
while (start <= end && IsStandardWhitespace(value[start])) {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the end index back past trailing standard whitespace.
|
||||||
|
while (end >= start && IsStandardWhitespace(value[end])) {
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value[start..(end + 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a character is one of the standard ASCII whitespace characters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="character">The character to inspect.</param>
|
||||||
|
/// <returns><see langword="true"/> for standard whitespace.</returns>
|
||||||
|
private static bool IsStandardWhitespace(char character) {
|
||||||
|
return character is ' ' or '\t' or '\r' or '\n' or '\v' or '\f';
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Nested types
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the character offsets of a matched bracket pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Start">The zero-based index of the opening bracket.</param>
|
||||||
|
/// <param name="End">The zero-based index of the closing bracket.</param>
|
||||||
|
private sealed record BracketPair(int Start, int End);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an opening bracket waiting for its matching closing bracket.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Index">The zero-based index of the opening bracket.</param>
|
||||||
|
/// <param name="Character">The opening bracket character.</param>
|
||||||
|
private sealed record OpenBracket(int Index, char Character);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the fields extracted from a file name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Artist">The extracted artist, or an empty string when no artist was found.</param>
|
||||||
|
/// <param name="Title">The extracted title, or an empty string when no title text remains.</param>
|
||||||
|
/// <param name="Comment">The combined comment text, or an empty string when no comment was extracted.</param>
|
||||||
|
public sealed record FilenameParseResult(string Artist, string Title, string Comment);
|
||||||
Reference in new issue
Block a user