< Summary

Information
Class: MusicTheory.Theory.Time.DurationNotation
Assembly: MusicTheory
File(s): /home/runner/work/MusicTheory/MusicTheory/Theory/Time/DurationNotation.cs
Line coverage
73%
Covered lines: 56
Uncovered lines: 20
Coverable lines: 76
Total lines: 154
Line coverage: 73.6%
Branch coverage
63%
Covered branches: 55
Total branches: 87
Branch coverage: 63.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryParse(...)80.55%363696.77%
Parse(...)0%620%
ToNotation(...)83.33%6681.81%
TryParseAbbrev(...)64%1232546.15%
TryParseFraction(...)50%88100%
AbbrevFor(...)10%431030.76%

File(s)

/home/runner/work/MusicTheory/MusicTheory/Theory/Time/DurationNotation.cs

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.Text.RegularExpressions;
 4
 5namespace MusicTheory.Theory.Time;
 6
 7/// <summary>
 8/// Duration の簡易文字列表現パーサ/フォーマッタ。
 9/// サポート:
 10/// - 基本値の略号: W(whole), H(half), Q(quarter), E(eighth), S(sixteenth), T(thirty-second), X(sixty-fourth), O(128th), D/B(d
 11/// - 分数表記: "1/4", "3/8" など(Whole=1 を基準)
 12/// - 付点: 終端の '.' を 1〜3 個(例: "Q.", "1/8..")
 13/// - 連符: 末尾に "(a:b)" または "*a:b" を付与(例: "E(3:2)", "1/8*5:4")
 14/// 未対応/不正な表記は TryParse=false を返し、Parse は ArgumentException を投げます。
 15/// </summary>
 16public static class DurationNotation
 17{
 18    public static bool TryParse(string text, out Duration duration)
 19    {
 820        duration = default;
 821        if (string.IsNullOrWhiteSpace(text)) return false;
 822        var s = text.Trim();
 23
 24        // 1) 連符抽出: (a:b) or *a:b (末尾にあるとみなす)
 825        Tuplet? tuplet = null;
 826        var mParen = Regex.Match(s, "\\((\\d+)\\s*:\\s*(\\d+)\\)\\s*$");
 827        var mStar = !mParen.Success ? Regex.Match(s, "\\*(\\d+)\\s*:\\s*(\\d+)\\s*$") : Match.Empty;
 828        if (mParen.Success || mStar.Success)
 29        {
 330            var m = mParen.Success ? mParen : mStar;
 331            if (!int.TryParse(m.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var a)) return fal
 332            if (!int.TryParse(m.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var b)) return fal
 333            if (a <= 0 || b <= 0) return false;
 334            tuplet = new Tuplet(a, b);
 335            s = s.Substring(0, (mParen.Success ? mParen.Index : mStar.Index)).TrimEnd();
 36        }
 37
 38        // 2) 付点抽出: 末尾 '.' を数える(最大3)
 839        int dots = 0;
 2540        for (int i = s.Length - 1; i >= 0 && s[i] == '.'; i--) dots++;
 841        if (dots > Duration.MaxDots) return false;
 1042        if (dots > 0) s = s.Substring(0, s.Length - dots).TrimEnd();
 43
 44        // 3) 本体: 略号 もしくは 分数 n/d
 45        RationalFactor fraction;
 846        if (TryParseAbbrev(s, out var baseValue))
 47        {
 648            var (n, d) = baseValue.GetFraction();
 649            fraction = new RationalFactor(n, d);
 50        }
 251        else if (TryParseFraction(s, out var num, out var den))
 52        {
 253            fraction = new RationalFactor(num, den);
 54        }
 55        else
 56        {
 057            return false;
 58        }
 59
 60        // 4) 付点適用
 861        if (dots > 0)
 62        {
 263            int pow = 1 << dots; // 2^dots
 264            int numMul = 2 * pow - 1;
 265            fraction = fraction * new RationalFactor(numMul, pow);
 66        }
 67
 68        // 5) 連符適用
 869        if (tuplet.HasValue)
 70        {
 371            fraction = fraction * tuplet.Value.FactorRational; // normal/actual
 72        }
 73
 874        duration = new Duration(fraction);
 875        return true;
 76    }
 77
 78    public static Duration Parse(string text)
 079        => TryParse(text, out var d) ? d : throw new ArgumentException($"Invalid duration notation: '{text}'");
 80
 81    /// <summary>
 82    /// Duration を簡易記法へフォーマット。分解に成功すれば略号+付点(+連符)を返し、失敗時は既約分数で返す。
 83    /// 例: Quarter+dots1 → "Q." / Eighth triplet → "E(3:2)" / 既知以外 → "num/den"
 84    /// </summary>
 85    public static string ToNotation(Duration duration, bool extendedTuplets = false)
 86    {
 387        if (duration.TryDecomposeFull(out var baseValue, out var dots, out var tuplet, extendedTuplets))
 88        {
 389            string core = AbbrevFor(baseValue);
 390            core += new string('.', dots);
 391            if (tuplet.HasValue)
 92            {
 293                var t = tuplet.Value;
 294                if (extendedTuplets)
 95                {
 196                    core += $"*{t.ActualCount}:{t.NormalCount}";
 97                }
 98                else
 99                {
 1100                    core += $"({t.ActualCount}:{t.NormalCount})";
 101                }
 102            }
 3103            return core;
 104        }
 105        // fallback: fraction
 0106        var f = duration.WholeFraction;
 0107        return $"{f.Numerator}/{f.Denominator}";
 108    }
 109
 110    private static bool TryParseAbbrev(string s, out BaseNoteValue value)
 111    {
 8112        value = default;
 8113        if (string.IsNullOrEmpty(s)) return false;
 8114        switch (s.Trim().ToUpperInvariant())
 115        {
 0116            case "D": case "B": value = BaseNoteValue.DoubleWhole; return true; // Breve/Double
 0117            case "W": value = BaseNoteValue.Whole; return true;
 0118            case "H": value = BaseNoteValue.Half; return true;
 4119            case "Q": value = BaseNoteValue.Quarter; return true;
 8120            case "E": value = BaseNoteValue.Eighth; return true;
 0121            case "S": value = BaseNoteValue.Sixteenth; return true;
 0122            case "T": value = BaseNoteValue.ThirtySecond; return true;
 0123            case "X": value = BaseNoteValue.SixtyFourth; return true;
 0124            case "O": value = BaseNoteValue.OneHundredTwentyEighth; return true;
 2125            default: return false;
 126        }
 127    }
 128
 129    private static bool TryParseFraction(string s, out int num, out int den)
 130    {
 2131        num = den = 0;
 2132        var parts = s.Split('/', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
 2133        if (parts.Length != 2) return false;
 2134        if (!int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) return false;
 2135        if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out den)) return false;
 2136        if (den == 0) return false;
 2137        return true;
 138    }
 139
 140    private static string AbbrevFor(BaseNoteValue v)
 3141        => v switch
 3142        {
 0143            BaseNoteValue.DoubleWhole => "D",
 0144            BaseNoteValue.Whole => "W",
 0145            BaseNoteValue.Half => "H",
 0146            BaseNoteValue.Quarter => "Q",
 3147            BaseNoteValue.Eighth => "E",
 0148            BaseNoteValue.Sixteenth => "S",
 0149            BaseNoteValue.ThirtySecond => "T",
 0150            BaseNoteValue.SixtyFourth => "X",
 0151            BaseNoteValue.OneHundredTwentyEighth => "O",
 0152            _ => "?"
 3153        };
 154}