From b901f55015090b3a76ab046d4d727f58826f8cc9 Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Thu, 2 Jul 2026 12:17:57 +1000 Subject: [PATCH] [patch] docs: add per-package READMEs and turn root README into a family index Each publishable Semantics package now ships its own README.md grounded in its actual public API (Strings, Strings.Identifiers, Paths, Quantities, Quantities.{Double,Float,Decimal}, Music, Color). The root README becomes a family overview with a package index table and short per-pillar teasers that link to each package README for full detail. --- README.md | 286 +++++++----------------- Semantics.Color/README.md | 160 +++++++++++++ Semantics.Music/README.md | 164 ++++++++++++++ Semantics.Paths/README.md | 180 +++++++++++++++ Semantics.Quantities.Decimal/README.md | 80 +++++++ Semantics.Quantities.Double/README.md | 80 +++++++ Semantics.Quantities.Float/README.md | 80 +++++++ Semantics.Quantities/README.md | 165 ++++++++++++++ Semantics.Strings.Identifiers/README.md | 101 +++++++++ Semantics.Strings/README.md | 186 +++++++++++++++ 10 files changed, 1274 insertions(+), 208 deletions(-) create mode 100644 Semantics.Color/README.md create mode 100644 Semantics.Music/README.md create mode 100644 Semantics.Paths/README.md create mode 100644 Semantics.Quantities.Decimal/README.md create mode 100644 Semantics.Quantities.Double/README.md create mode 100644 Semantics.Quantities.Float/README.md create mode 100644 Semantics.Quantities/README.md create mode 100644 Semantics.Strings.Identifiers/README.md create mode 100644 Semantics.Strings/README.md diff --git a/README.md b/README.md index 978d7a5..72622b0 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,36 @@ [![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) -A .NET library for replacing primitive obsession with strongly-typed, self-validating domain models. Four pillars: +A .NET family of libraries for replacing primitive obsession with strongly-typed, self-validating domain models. Each pillar ships as its own NuGet package, so you take only what you need. -- **Semantic Strings** — type-safe wrappers like `EmailAddress`, `UserId`, `BlogSlug` with attribute-driven validation, plus a batteries-included `Semantics.Strings.Identifiers` package (`Uuid`, `Ulid`, `Iban`, `Isbn`, `CreditCardNumber`, `JwtToken`). -- **Semantic Paths** — polymorphic `IPath` hierarchy for files, directories, absolute, relative, and combinations. -- **Semantic Quantities** — a metadata-generated, type-safe quantity system with a unified `IVector0..IVector4` model covering 60+ physical dimensions and 200+ generated types. Optional per-storage-type alias packages let you write `Mass` instead of `Mass`. -- **Semantic Music** — immutable musical value types: `PitchClass`, `Pitch`, `Interval`, `Mode`/`Scale`, `Chord`, `Key`, rational `Duration`, and `TimeSignature`, with chord-symbol parsing and voicing, plus a harmonic/structural **analysis** layer (progressions, cadences, key inference, sections, forms). +- **Semantic Strings**: type-safe wrappers like `EmailAddress`, `UserId`, `BlogSlug` with attribute-driven validation, plus a batteries-included identifiers package (`Uuid`, `Ulid`, `Iban`, `Isbn`, `CreditCardNumber`, `JwtToken`). +- **Semantic Paths**: a polymorphic `IPath` hierarchy for files, directories, absolute, relative, and combinations. +- **Semantic Quantities**: a metadata-generated, type-safe quantity system with a unified `IVector0..IVector4` model covering 60+ physical dimensions and 200+ generated types. Optional per-storage-type alias packages let you write `Mass` instead of `Mass`. +- **Semantic Music**: immutable musical value types (`Pitch`, `Interval`, `Scale`, `Chord`, `Key`, `Duration`, `TimeSignature`), with chord-symbol parsing and voicing, plus a harmonic and structural analysis layer. +- **Semantic Color**: a physically-grounded `Color` type with linear-RGB math, perceptual Oklab operations, and built-in WCAG accessibility checks. -Targets `net8.0`–`net10.0`. Semantic Strings, Paths, and Music additionally target `netstandard2.0`/`netstandard2.1`; Semantic Quantities is `net8.0`+ (it requires `INumber`). +## Packages -## Install +Each package has its own README with the full API surface, examples, and reference tables. This document is the family overview. -```bash -dotnet add package ktsu.Semantics -``` +| Package | What it provides | Details | +|---------|------------------|---------| +| [`ktsu.Semantics.Strings`](Semantics.Strings/README.md) | `SemanticString` framework and validation attributes | [README](Semantics.Strings/README.md) | +| [`ktsu.Semantics.Strings.Identifiers`](Semantics.Strings.Identifiers/README.md) | Ready-made identifier types (`Uuid`, `Iban`, `Isbn`, ...) | [README](Semantics.Strings.Identifiers/README.md) | +| [`ktsu.Semantics.Paths`](Semantics.Paths/README.md) | Polymorphic, typed file system path types | [README](Semantics.Paths/README.md) | +| [`ktsu.Semantics.Quantities`](Semantics.Quantities/README.md) | Generated, dimensionally-safe physical quantities | [README](Semantics.Quantities/README.md) | +| [`ktsu.Semantics.Quantities.Double`](Semantics.Quantities.Double/README.md) | `double` storage-type aliases | [README](Semantics.Quantities.Double/README.md) | +| [`ktsu.Semantics.Quantities.Float`](Semantics.Quantities.Float/README.md) | `float` storage-type aliases | [README](Semantics.Quantities.Float/README.md) | +| [`ktsu.Semantics.Quantities.Decimal`](Semantics.Quantities.Decimal/README.md) | `decimal` storage-type aliases | [README](Semantics.Quantities.Decimal/README.md) | +| [`ktsu.Semantics.Music`](Semantics.Music/README.md) | Musical value types and harmonic analysis | [README](Semantics.Music/README.md) | +| [`ktsu.Semantics.Color`](Semantics.Color/README.md) | Linear/perceptual color with accessibility tooling | [README](Semantics.Color/README.md) | + +All packages target `net8.0`–`net10.0`. Strings, Identifiers, Paths, Music, and Color additionally target `netstandard2.0`/`netstandard2.1`. Quantities and its alias packages are `net8.0`+ (they require `INumber`). ## Semantic strings +Define a string-shaped domain type, attach validation attributes, and get a compile-time-distinct type that validates on construction. + ```csharp using ktsu.Semantics.Strings; @@ -34,233 +47,103 @@ public sealed record EmailAddress : SemanticString { } [StartsWith("USER_"), HasNonWhitespaceContent] public sealed record UserId : SemanticString { } -// Direct construction — no generic params needed -var email = EmailAddress.Create("user@example.com"); -var userId = UserId.Create("USER_12345"); - -// Span-based and char[] overloads exist too -var email2 = EmailAddress.Create("user@example.com".AsSpan()); - -// Safe creation -if (EmailAddress.TryCreate("maybe@invalid", out EmailAddress? safe)) { /* … */ } - -// Compile-time safety -public void SendWelcomeEmail(EmailAddress to, UserId userId) { /* … */ } -// SendWelcomeEmail(userId, email); // ❌ compiler error -``` - -### Combining attributes +EmailAddress email = EmailAddress.Create("user@example.com"); +UserId userId = UserId.Create("USER_12345"); -```csharp -// All must pass (default) -[IsEmailAddress, EndsWith(".com")] -public sealed record DotComEmail : SemanticString { } - -// Any can pass -[ValidateAny] -[IsEmailAddress, IsUri] -public sealed record ContactMethod : SemanticString { } +public void SendWelcomeEmail(EmailAddress to, UserId who) { /* ... */ } +// SendWelcomeEmail(userId, email); // does not compile ``` -The full attribute catalogue (text, format, casing, first-class .NET types, paths) lives in [`docs/validation-reference.md`](docs/validation-reference.md). - -### Batteries-included identifiers - -Defining your own types (as above) is the framework's core use, but the `ktsu.Semantics.Strings.Identifiers` package ships ready-made identifier types so common cases need no boilerplate: +The [`ktsu.Semantics.Strings.Identifiers`](Semantics.Strings.Identifiers/README.md) package adds ready-made identifier types with real check-digit and structural validation: ```csharp using ktsu.Semantics.Strings.Identifiers; -Uuid id = Uuid.Create("123E4567-E89B-12D3-A456-426614174000"); // canonicalised to lowercase -Ulid ulid = Ulid.Create("01ARZ3NDEKTSV4RRFFQ69G5FAV"); -CreditCardNumber card = CreditCardNumber.Create("4111 1111 1111 1111"); // separators stripped, Luhn-checked -Isbn isbn = Isbn.Create("978-0-306-40615-7"); // ISBN-10 or ISBN-13, check-digit validated -Iban iban = Iban.Create("GB82 WEST 1234 5698 7654 32"); // whitespace stripped, mod-97 validated - -Uuid.Create("not-a-uuid"); // ❌ throws ArgumentException +Uuid id = Uuid.Create("123E4567-E89B-12D3-A456-426614174000"); // canonicalised to lowercase +Iban iban = Iban.Create("GB82 WEST 1234 5698 7654 32"); // whitespace stripped, mod-97 validated ``` -Each type normalises on creation and validates with real check-digit maths where applicable (`Iban`, `Isbn`, `CreditCardNumber`) or structural/format rules (`Uuid`, `Ulid`, `JwtToken`). Validation is pragmatic with documented limits rather than RFC-exhaustive — for example, `JwtToken` checks the three-segment structure and that the header and payload decode to JSON objects, but does **not** verify the signature. +Full detail: [Semantics.Strings README](Semantics.Strings/README.md) and [Semantics.Strings.Identifiers README](Semantics.Strings.Identifiers/README.md). ## Semantic paths +A path is a type that encodes whether it names a file or a directory and whether it is absolute or relative. Compose them with `/`, decompose with typed properties, and hold mixed paths behind `IPath`. + ```csharp using ktsu.Semantics.Paths; -var configFile = AbsoluteFilePath.Create(@"C:\app\config.json"); - -configFile.FileName; // config.json -configFile.FileExtension; // .json -configFile.DirectoryPath; // C:\app -configFile.Exists; // bool - -// Polymorphic collections -List all = [ - AbsoluteFilePath.Create(@"C:\data.txt"), - RelativeDirectoryPath.Create(@"logs\app"), - FilePath.Create(@"document.pdf"), -]; +AbsoluteDirectoryPath projectDir = AbsoluteDirectoryPath.Create(@"C:\repos\app"); +AbsoluteFilePath source = projectDir / RelativeFilePath.Create(@"src\Program.cs"); -var files = all.OfType().ToList(); -var absolutes = all.OfType().ToList(); +FileName name = source.FileName; // Program.cs +FileExtension ext = source.FileExtension; // .cs +RelativeFilePath rel = source.AsRelative(projectDir); ``` -Conversions: `AsAbsolute()`, `AsAbsolute(baseDirectory)`, `AsRelative(baseDirectory)`. +Full detail: [Semantics.Paths README](Semantics.Paths/README.md). ## Semantic quantities -Every quantity is a vector. Direction-space dimensionality is part of the type: - -| Form | Sign | Examples | -|---|---|---| -| `IVector0` (magnitude) | `>= 0` | `Speed`, `Mass`, `Energy`, `Distance`, `Area` | -| `IVector1` (signed 1D) | signed | `Velocity1D`, `Force1D`, `Temperature` | -| `IVector2` (2D) | per-component | `Velocity2D`, `Force2D`, `Acceleration2D` | -| `IVector3` (3D) | per-component | `Velocity3D`, `Force3D`, `Position3D` | -| `IVector4` (4D) | per-component | reserved (relativistic / spacetime) | +Every quantity is a vector whose direction-space dimensionality is part of the type. Operators flow from a metadata definition, so `Force · Displacement` yields `Energy` at compile time. ```csharp using ktsu.Semantics.Quantities; -// V0 magnitudes — From{Unit} factories use the singular lemma (#49) -var speed = Speed.FromMeterPerSecond(15.0); -var mass = Mass.FromKilogram(10.0); -var distance = Distance.FromMeter(5.0); - -// V3 directional — object-initializer syntax (X/Y/Z components) -var force3d = new Force3D { X = 0.0, Y = 0.0, Z = -9.8 }; -var disp3d = new Displacement3D { X = 3.0, Y = 4.0, Z = 0.0 }; - -// Operators flow from dimensions.json -var work = ForceMagnitude.FromNewton(10.0) * distance; // F·d = Energy -var power = work / Duration.FromSecond(2.0); // W/t = Power +Mass m = Mass.FromKilogram(2.0); +Speed v = Speed.FromMeterPerSecond(3.0); +MomentumMagnitude p = m * v; // Mass * Speed -> MomentumMagnitude -// Vector ops -var workDot = force3d.Dot(disp3d); // Energy -var torque = force3d.Cross(disp3d); // Torque3D -var mag = disp3d.Magnitude(); // Length (always >= 0) - -// Construction-time invariants (#50, #51) -// Speed.FromMeterPerSecond(-1.0) // ArgumentException — V0 must be non-negative -// Wavelength.FromMeter(0.0) // ArgumentException — strict-positive overload - -// Type safety -// var nope = force3d + speed; // ❌ compiler error +Force3D f = new() { X = 3.0, Y = 4.0, Z = 0.0 }; +ForceMagnitude mag = f.Magnitude(); // 5.0 ``` -Semantic overloads (e.g. `Weight` over `ForceMagnitude`, `Diameter` ↔ `Radius`): +If a project uses one storage type throughout, reference an alias package and drop the generic argument entirely: ```csharp -var raw = ForceMagnitude.FromNewton(686.0); -var weight = Weight.From(raw); // explicit narrow -ForceMagnitude back = weight; // implicit widen - -var radius = Radius.FromMeter(2.0); -var diameter = radius.ToDiameter(); // 4 m via metadata-defined relationship -``` - -Physical constants are exposed in two shapes: +using ktsu.Semantics.Quantities; // with ktsu.Semantics.Quantities.Double referenced -```csharp -// Domain-grouped, exact PreciseNumber values: -PhysicalConstants.Fundamental.SpeedOfLight; // 299_792_458 m/s as PreciseNumber -PhysicalConstants.Chemistry.GasConstant; // 8.31446... J/(mol·K) - -// Generic accessors — materialise into any T : INumber: -var c = PhysicalConstants.Generic.SpeedOfLight(); -var R = PhysicalConstants.Generic.GasConstant(); -``` - -### Storage-type alias packages - -Every quantity is generic over its storage type (`Mass`, `Speed`, …). If a project uses one storage type throughout, reference a satellite package and drop the generic argument entirely: - -```xml - -``` - -```csharp -using ktsu.Semantics.Quantities; - -// No anywhere — the package injects global-using aliases for every quantity. Mass mass = Mass.FromKilogram(10.0); Speed speed = Speed.FromMeterPerSecond(15.0); -Mass total = mass + Mass.FromKilogram(2.0); // still a Mass, full identity ``` -The aliases are real `Mass` (etc.), so they interoperate with the whole API with no conversion. Packages exist for `Double`, `Float`, and `Decimal` — reference exactly one per project to pick the storage type. The alias lists are generated from the quantity catalogue (`scripts/Generate-AliasProps.ps1`) and validated in CI. - -The unified vector model and its rationale: [`docs/strategy-unified-vector-quantities.md`](docs/strategy-unified-vector-quantities.md). -A per-domain tour: [`docs/physics-domains-guide.md`](docs/physics-domains-guide.md). -How the source generator turns `dimensions.json` into types: [`docs/physics-generator.md`](docs/physics-generator.md). +Full detail: [Semantics.Quantities README](Semantics.Quantities/README.md). The unified vector model and generator are documented in [`docs/strategy-unified-vector-quantities.md`](docs/strategy-unified-vector-quantities.md) and [`docs/physics-generator.md`](docs/physics-generator.md). ## Semantic music -Immutable, validated musical value types under `ktsu.Semantics.Music`. Pure logic, no I/O. The pitch convention is MIDI 60 = C4. +Immutable, validated musical value types plus an analysis layer that models harmony nested inside structure. Pure logic, no I/O. The pitch convention is MIDI 60 = C4. ```csharp using ktsu.Semantics.Music; -// Pitches and intervals -Pitch middleC = Pitch.FromName("C4"); // MIDI 60 -Pitch g4 = middleC.Transpose(7); // a perfect fifth up -Interval fifth = Interval.Between(middleC, g4); // +7 semitones - -// Scales and modes (diatonic, jazz, symmetric, pentatonic, blues) -Scale dDorian = Scale.Create(PitchClass.Create(2), Mode.Dorian); -bool hasF = dDorian.Contains(PitchClass.Create(5)); // true - -// Chord-symbol parsing → tones and voicings -Chord cmaj7 = Chord.Parse("Cmaj7#11"); -IReadOnlyList tones = cmaj7.ChordTones(); // 0,4,7,11,18 -IReadOnlyList voicing = Chord.Parse("C/G").Voice(4); // G3, C4, E4, G4 - -// Inversions and transposition -IReadOnlyList firstInv = Chord.Parse("C").Voice(4, 1); // E4, G4, C5 -Chord dmaj7 = Chord.Parse("Cmaj7").Transpose(2); // up a tone - -// Roman-numeral function within a key — and back again -Key cMajor = Key.Create(PitchClass.Create(0), Mode.Major); -string fn = cMajor.RomanNumeralOf(Chord.Parse("Dm7")); // "ii7" -Chord ii = cMajor.ChordFromRomanNumeral("ii7"); // Dm7 - -// Rational durations, time signatures (exact for tuplets), and real time -Duration tripletEighth = Duration.Create(1, 12); // three fill a quarter -Duration barOf7_8 = TimeSignature.Create(7, 8).BarDuration; -Note a4 = Note.Create(Pitch.FromName("A4"), Duration.Half, Velocity.Forte); -double seconds = a4.Seconds(Tempo.Create(120)); // 1.0 -double hz = a4.Pitch.FrequencyHz; // 440.0 +Progression prog = Progression.Parse("| Dm7 | G7 | Cmaj7 |"); +Key key = prog.InferKey()!; // C major +IReadOnlyList roman = prog.RomanNumerals(key); // ii7, V7, Imaj7 + +Chord cmaj7 = Chord.Parse("Cmaj7"); +IReadOnlyList voicing = cmaj7.Voice(octave: 4); // C4, E4, G4, B4 ``` -The chord parser covers triads, sixths, sevenths (including half-diminished `m7b5` and minor-major `mmaj7`), extensions and altered tensions (`9`/`11`/`13`, `b9`/`#9`/`#11`/`b13`), suspensions, power chords, omit voicings (`no3`/`no5`), and slash bass. Beyond the value types there are score primitives (`Note`, `Rest`, `Velocity`, `Tempo`) that convert rhythm to real time, equal-tempered `Pitch`↔frequency (A440), chord inversions, `Transpose` on `Chord`/`Scale`/`Key`, and roman-numeral parsing as the inverse of `RomanNumeralOf`. +Full detail: [Semantics.Music README](Semantics.Music/README.md). -### Analysis: progressions, sections, and forms +## Semantic color -Above the single-event types is an analysis layer that models harmony nested inside structure. A `Progression` is an ordered chord sequence with bar-based harmonic rhythm; it computes roman numerals, functional roles, cadences, an inferred key, and chromatic classifications. `Section`s group progressions into labelled units, an `Arrangement` orders them into a piece, and `Form` extracts the structural letter-pattern and names it. +The canonical `Color` stores linear RGBA, so mixing, interpolation, and luminance are physically correct. Perceptual work happens in Oklab, and WCAG accessibility tooling is built in. ```csharp -using ktsu.Semantics.Music; +using ktsu.Semantics.Color; -// A chord progression with bar-based harmonic rhythm ("|" = barline) -Progression prog = Progression.Parse("Dm7 | G7 | Cmaj7"); -Key key = prog.InferKey()!; // C major (quality-weighted fit) +Color text = Color.FromHex("#777777"); +Color background = NamedColors.White; -IReadOnlyList roman = prog.RomanNumerals(key); // "ii7", "V7", "Imaj7" -IReadOnlyList fns = prog.Functions(key); // Predominant, Dominant, Tonic -IReadOnlyList cadences = prog.Cadences(key); // Authentic (V→I) at the resolution - -// Chromatic analysis: secondary dominants, borrowed chords, Neapolitan -IReadOnlyList chromatic = - Progression.Parse("C | D7 | G7 | C").ChromaticChords(key); // D7 → "V/V" (SecondaryDominant) +if (text.AccessibilityLevelAgainst(background) < AccessibilityLevel.AA) +{ + text = text.AdjustForContrast(background, AccessibilityLevel.AA); +} -// Structure: sections → arrangement → form -Section verse = Section.Create(SectionType.Verse, Progression.Parse("C | Am | F | G")); -Section bridge = Section.Create(SectionType.Bridge, Progression.Parse("F | G | Em | Am")); -Arrangement song = Arrangement.Create(key, [verse, verse, bridge, verse]); -Form form = song.Form; // Pattern "AABA", Name ThirtyTwoBarAABA +IReadOnlyList ramp = NamedColors.Red.Gradient(NamedColors.Blue, 5); // Oklab gradient ``` -Cadences are classified by scale-degree motion (authentic, plagal, half, deceptive); key inference is quality-weighted so it distinguishes a key from its relative/parallel neighbours; `Form` recognizes named forms including AABA, ternary, binary, rondo, strophic, and the 12-bar blues progression template. +Full detail: [Semantics.Color README](Semantics.Color/README.md). ## Dependency injection @@ -269,48 +152,35 @@ services.AddTransient, SemanticStringFactor public class UserService(ISemanticStringFactory emails) { - public Task CreateUserAsync(string raw) => - emails.TryCreate(raw, out var email) - ? Task.FromResult(new User(email)) + public User CreateUser(string raw) => + emails.TryFromString(raw, out EmailAddress? email) + ? new User(email!) : throw new ArgumentException("invalid email"); } ``` ## Architecture -The physics system is **metadata-driven**. The single source of truth is -`Semantics.SourceGenerators/Metadata/dimensions.json` (with `units.json`, `magnitudes.json`, `conversions.json`, and `domains.json` alongside it), and a Roslyn incremental generator emits: +The quantity system is metadata-driven. The single source of truth is `Semantics.SourceGenerators/Metadata/dimensions.json` (with `units.json`, `magnitudes.json`, `conversions.json`, `domains.json`, and `logarithmic.json` alongside it), and a Roslyn incremental generator emits the quantity records, unit-conversion factories, cross-dimensional operators, and physical constants. Generated output is committed to `Semantics.Quantities/Generated/` so the project compiles without first running the generator. -- One record per quantity (Vector0/1/2/3/4 base + semantic overloads). -- A `From{Unit}` factory per declared unit, with built-in unit conversion to the SI base unit and a `Vector0Guards` enforce-non-negative (or strict-positive) check on V0 types. -- Cross-dimensional `*`, `/`, `Dot`, `Cross` operators driven by `integrals` / `derivatives` / `dotProducts` / `crossProducts` declarations. -- `PhysicalConstants` with both domain-grouped `PreciseNumber` fields and generic `T.CreateChecked`-backed accessors. - -Generator diagnostics catch metadata problems at build time: - -- **SEM001** — relationship references an unknown dimension name. -- **SEM002** — schema-level metadata issue (missing fields, duplicate type names, etc). -- **SEM003** — relationship's `forms` list references a vector form not declared on a participating dimension. -- **SEM004** — `availableUnits` references a unit not declared in `units.json`. - -Generated output is committed to `Semantics.Quantities/Generated/` so the project compiles without first running the generator. - -The string and path systems use the same building blocks: an attribute → strategy → rule → factory pipeline. See [`docs/architecture.md`](docs/architecture.md). +The string and path systems share an attribute → strategy → rule → factory validation pipeline. See [`docs/architecture.md`](docs/architecture.md). ## Documentation -- [Complete library guide](docs/complete-library-guide.md) — start here for a feature tour. +- [Complete library guide](docs/complete-library-guide.md) (start here for a feature tour). - [Architecture (strings/paths/validation)](docs/architecture.md) -- [Architecture (physics — unified vector model)](docs/strategy-unified-vector-quantities.md) +- [Architecture (physics, unified vector model)](docs/strategy-unified-vector-quantities.md) - [Source-generator workflow](docs/physics-generator.md) - [Physics domains tour](docs/physics-domains-guide.md) - [Validation attributes reference](docs/validation-reference.md) - [Advanced usage patterns](docs/advanced-usage.md) +Per-package API detail lives in each package's own README, linked from the [Packages](#packages) table above. + ## Contributing -Contributions are welcome — please open an issue first for major changes so we can discuss the direction. The branch's open work items are tracked as GitHub issues. +Contributions are welcome. Please open an issue first for major changes so we can discuss the direction. The branch's open work items are tracked as GitHub issues. ## License -MIT — see [`LICENSE.md`](LICENSE.md). +MIT. See [`LICENSE.md`](LICENSE.md). diff --git a/Semantics.Color/README.md b/Semantics.Color/README.md new file mode 100644 index 0000000..e17f9ae --- /dev/null +++ b/Semantics.Color/README.md @@ -0,0 +1,160 @@ +# ktsu.Semantics.Color + +> A physically-grounded color type with linear-RGB math, perceptual Oklab operations, and built-in WCAG accessibility checks. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Color?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Color) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Color?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Color) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Color?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Color) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Color` is one package in the [ktsu.Semantics](../README.md) family. Start at the [root README](../README.md) for the family overview. + +## Introduction + +`ktsu.Semantics.Color` treats color the way rendering and color science do: the canonical `Color` type stores linear (non gamma-encoded) RGB plus straight alpha, each channel in 0..1, and all math happens in linear space. Gamma-encoded sRGB is a separate type (`Srgb`) that you cross into only when you convert. That distinction is the difference between correct blending, mixing, and luminance and the subtly-wrong results you get from doing arithmetic on gamma-encoded values. + +On top of that foundation the package adds perceptual operations in the Oklab color space, hex and byte interop, `System.Numerics` vector output, and WCAG contrast tooling that can rate a color pair and automatically nudge a color until it meets a target conformance level. + +## Features + +- **Linear RGB as the hub**: `Color` stores linear RGBA in 0..1, and all mixing, interpolation, and luminance math happens there. +- **Color spaces**: sRGB (`Srgb`), HSL (`Hsl`), HSV (`Hsv`), Oklab (`Oklab`), and Oklch (`Oklch`), each with `From*` / `To*` conversions on `Color`. +- **Interop**: hex parse and format (`#RGB`, `#RRGGBB`, `#RRGGBBAA`), 8-bit byte tuples, and linear or sRGB `Vector3` / `Vector4` output (the sRGB vectors are what ImGui expects). +- **WCAG accessibility**: relative luminance, contrast ratio (1..21), conformance rating against a background, and `AdjustForContrast` which binary-searches Oklab lightness to hit a target while preserving hue and chroma. +- **Perceptual operations**: Oklab distance (`DistanceTo`), perceptually uniform mixing (`MixOklab`), and Oklab gradients (`Gradient`), alongside plain linear `Lerp`. +- **Named colors**: a CSS/X11 subset with case-insensitive lookup. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Color +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Color +``` + +### Package Reference + +```xml + +``` + +## Usage Examples + +### Basic Example: accessible text color + +```csharp +using ktsu.Semantics.Color; + +Color text = Color.FromHex("#777777"); +Color background = NamedColors.White; + +double ratio = text.ContrastRatio(background); // ~4.48 +AccessibilityLevel level = text.AccessibilityLevelAgainst(background); // Fail for AA body text + +if (level < AccessibilityLevel.AA) +{ + // darkens or lightens in Oklab, preserving hue and chroma, until AA is met + text = text.AdjustForContrast(background, AccessibilityLevel.AA); +} + +Console.WriteLine(text.ToHex()); +``` + +### Perceptually uniform gradients + +```csharp +using ktsu.Semantics.Color; + +Color start = Color.FromSrgb(0.9, 0.1, 0.2); // sRGB input, stored as linear +Color end = NamedColors.Blue; + +// Oklab interpolation, inclusive of both endpoints; steps must be >= 2 +IReadOnlyList ramp = start.Gradient(end, 5); + +foreach (Color c in ramp) +{ + (byte r, byte g, byte b, byte a) = c.ToBytes(); + Console.WriteLine($"{c.ToHex()} rgba({r},{g},{b},{a})"); +} +``` + +### Space conversions and vector interop + +```csharp +using System.Numerics; +using ktsu.Semantics.Color; + +Color c = NamedColors.Orange; + +Hsl hsl = c.ToHsl(); // H in degrees, S/L in 0..1 +Color complementary = Color.FromHsl(hsl with { H = (hsl.H + 180) % 360 }); + +Oklch lch = c.ToOklch(); // perceptual lightness/chroma/hue +Color brighter = Color.FromOklch(lch with { L = lch.L + 0.1 }); + +Vector4 imguiColor = complementary.ToSrgbVector4(); // gamma-encoded RGBA for ImGui +``` + +## API Reference + +### `Color` + +The canonical color: linear RGBA, each channel `double` in 0..1. A `readonly record struct` with positional properties `R`, `G`, `B`, `A`. + +#### Construction and conversion + +| Name | Return Type | Description | +|------|-------------|-------------| +| `FromLinear(r, g, b, a = 1)` | `Color` | From linear RGBA. | +| `FromSrgb(r, g, b, a = 1)` / `FromSrgb(Srgb, a = 1)` | `Color` | From gamma-encoded sRGB. | +| `FromBytes(r, g, b, a = 255)` | `Color` | From 8-bit channels. | +| `FromHex(string)` | `Color` | Parse `#RGB`, `#RRGGBB`, or `#RRGGBBAA` (the `#` is optional). | +| `FromOklab` / `FromOklch` / `FromHsl` / `FromHsv` | `Color` | From the named space. | +| `ToSrgb()` / `ToOklab()` / `ToOklch()` / `ToHsl()` / `ToHsv()` | space type | Convert out. | +| `ToHex()` | `string` | `#RRGGBB`, or `#RRGGBBAA` when alpha is not full. | +| `ToBytes()` | `(byte, byte, byte, byte)` | Rounded 8-bit RGBA. | +| `ToLinearVector3/4()` / `ToSrgbVector3/4()` | `Vector3` / `Vector4` | `System.Numerics` interop. | +| `WithAlpha(a)` / `Clamp()` | `Color` | Copy helpers. | + +#### Operations + +| Name | Return Type | Description | +|------|-------------|-------------| +| `RelativeLuminance` | `double` | WCAG luminance. | +| `ContrastRatio(other)` | `double` | WCAG ratio, 1..21. | +| `AccessibilityLevelAgainst(background, largeText = false)` | `AccessibilityLevel` | Conformance rating. | +| `AdjustForContrast(background, target, largeText = false)` | `Color` | Nudge lightness to meet a target level. | +| `DistanceTo(other)` | `double` | Perceptual Oklab distance. | +| `MixOklab(other, t)` | `Color` | Perceptually uniform mix (`t = 0` returns this, `t = 1` returns other). | +| `Lerp(other, t)` | `Color` | Linear-RGB interpolation. | +| `Gradient(to, steps)` | `IReadOnlyList` | Oklab gradient, inclusive of endpoints (`steps >= 2`). | + +### Supporting types + +| Type | Description | +|------|-------------| +| `Srgb` | Gamma-encoded sRGB, the only gamma-boundary crossing. `ToLinear()`, `FromLinear(Color)`. | +| `Hsl` / `Hsv` | Hue in degrees, saturation and lightness/value in 0..1, defined over sRGB. `FromSrgb` / `ToSrgb`. | +| `Oklab` | Perceptual color space (Ottosson 2020). `FromColor` / `ToColor` / `ToOklch`. | +| `Oklch` | Polar form of Oklab. `ToOklab`. | +| `AccessibilityLevel` | enum: `Fail = 0`, `AA = 1`, `AAA = 2`. | +| `NamedColors` | Common colors (`Black`, `White`, `Red`, `Orange`, `Transparent`, ...), plus `All` and `TryGet(name, out color)` with case-insensitive keys. | + +`FromHex` throws `ArgumentException` for lengths other than 3, 6, or 8, and `Gradient` throws `ArgumentException` for `steps < 2`. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Music/README.md b/Semantics.Music/README.md new file mode 100644 index 0000000..6483173 --- /dev/null +++ b/Semantics.Music/README.md @@ -0,0 +1,164 @@ +# ktsu.Semantics.Music + +> Immutable, validated musical value types plus a harmonic and structural analysis layer, all pure logic with no I/O. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Music?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Music) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Music?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Music) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Music?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Music) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Music` is one package in the [ktsu.Semantics](../README.md) family. Start at the [root README](../README.md) for the family overview. + +## Introduction + +`ktsu.Semantics.Music` models music theory as immutable value types. Pitches, intervals, scales, modes, chords, keys, rational durations, and time signatures are all first-class, validate on creation, and return new instances on every operation. The pitch convention is MIDI 60 = C4, and equal temperament is anchored at A4 = 440 Hz. + +Above the single-event types sits an analysis layer that models harmony nested inside structure. A `Progression` is an ordered chord sequence with bar-based harmonic rhythm that computes roman numerals, functional roles, cadences, an inferred key, and chromatic classifications. `Section`s group progressions, an `Arrangement` orders them into a piece, and `Form` names the structural pattern. + +## Features + +- **Pitch and interval types**: `PitchClass`, `Pitch` (MIDI, with name and frequency conversion), `Interval` (signed semitones, cents, folding). +- **Scales and modes**: `Mode` with roughly 29 presets (diatonic, jazz, symmetric, pentatonic, blues), `Scale` rooting a mode at a pitch class, with `Contains` and `DegreeOf`. +- **Chord-symbol parsing**: `Chord.Parse` handles triads, sixths, sevenths (including `m7b5` and `mmaj7`), extensions and altered tensions (`9`/`11`/`13`, `b9`/`#9`/`#11`/`b13`), suspensions, power chords, omissions (`no3`/`no5`), and slash bass. +- **Chord realization**: `ChordTones()` and `Voice(octave)` / `Voice(octave, inversion)`, plus `Transpose`. +- **Roman-numeral analysis both directions**: `Key.RomanNumeralOf(chord)` and `Key.ChordFromRomanNumeral(numeral)`. +- **Rhythm and real time**: rational `Duration`, `TimeSignature`, `Tempo`, and `Note` / `Rest` / `ChordEvent` events that convert to seconds. +- **Analysis layer**: `Progression` (roman numerals, functions, cadences, key inference, chromatic chords), `Section`, `Arrangement`, and `Form` with named-form recognition. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Music +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Music +``` + +### Package Reference + +```xml + +``` + +## Usage Examples + +### Pitches, intervals, scales + +```csharp +using ktsu.Semantics.Music; + +Pitch middleC = Pitch.FromName("C4"); // MIDI 60 +Pitch g4 = middleC.Transpose(7); // a perfect fifth up +Interval fifth = Interval.Between(middleC, g4); // +7 semitones +double hz = Pitch.FromName("A4").FrequencyHz; // 440.0 + +Scale dDorian = Scale.Create(PitchClass.Create(2), Mode.Dorian); +bool hasF = dDorian.Contains(PitchClass.Create(5)); // true +``` + +### Chord parsing, voicing, and rhythm + +```csharp +using ktsu.Semantics.Music; + +Chord cmaj7 = Chord.Parse("Cmaj7"); +IReadOnlyList voicing = cmaj7.Voice(octave: 4); // C4, E4, G4, B4 +IReadOnlyList tones = Chord.Parse("Cmaj7#11").ChordTones(); +Chord up = cmaj7.Transpose(2); // Dmaj7 + +Tempo tempo = Tempo.Create(120.0); // quarter = 120 bpm +double halfNoteSeconds = tempo.Seconds(Duration.Half); // 1.0 s +Note a4 = Note.Create(Pitch.FromName("A4"), Duration.Quarter, Velocity.Forte); +double noteSeconds = a4.Seconds(tempo); // 0.5 s +``` + +### Roman numerals, both directions + +```csharp +using ktsu.Semantics.Music; + +Key cMajor = Key.Create(PitchClass.Create(0), Mode.Major); + +string label = cMajor.RomanNumeralOf(Chord.Parse("Dm7")); // "ii7" +Chord five = cMajor.ChordFromRomanNumeral("V7"); // G7 +``` + +### Analysis: progressions, cadences, key inference, and form + +```csharp +using ktsu.Semantics.Music; + +// "|" is a barline; spaces separate chords within a bar +Progression prog = Progression.Parse("| Dm7 | G7 | Cmaj7 | Cmaj7 |"); + +Key key = prog.InferKey()!; // C major (quality-weighted fit) +IReadOnlyList roman = prog.RomanNumerals(key); // ii7, V7, Imaj7, Imaj7 +IReadOnlyList fns = prog.Functions(key); // Predominant, Dominant, Tonic, Tonic +IReadOnlyList cadences = prog.Cadences(key); // Authentic at the resolution + +// Chromatic analysis: secondary dominants, borrowed chords, Neapolitan +IReadOnlyList chromatic = + Progression.Parse("| C | A7 | Dm | G7 |").ChromaticChords(key); // A7 -> secondary dominant + +// Structure: sections -> arrangement -> form +Progression verse = Progression.Parse("| C | G | Am | F |"); +Progression bridge = Progression.Parse("| F | C | G | C |"); +Arrangement song = Arrangement.Create(key, +[ + Section.Create(SectionType.Verse, verse, "Verse 1"), + Section.Create(SectionType.Verse, verse, "Verse 2"), + Section.Create(SectionType.Bridge, bridge, "Bridge"), + Section.Create(SectionType.Verse, verse, "Verse 3"), +]); +Form form = song.Form; // Pattern "AABA", Name NamedForm.ThirtyTwoBarAABA +``` + +## API Reference + +### Core value types + +| Type | Description | Key factories / members | +|------|-------------|-------------------------| +| `PitchClass` | One of twelve pitch classes, folded to 0..11. | `Create(int)`, `Value`, `Name` | +| `Pitch` | MIDI pitch 0..127 (60 = C4). | `Create(int)`, `FromName(string)`, `FromFrequency(double)`, `Transpose(int)`, `FrequencyHz`, `Octave` | +| `Interval` | Signed interval in semitones. | `Create(int)`, `Between(Pitch, Pitch)`, `Semitones`, `Cents`, `Folded` | +| `Mode` | Scale shape by semitone offsets. | `FromName(string)`, presets (`Major`, `Dorian`, `HarmonicMinor`, `WholeTone`, `MajorPentatonic`, ...), `Intervals` | +| `Scale` | A mode rooted at a pitch class. | `Create(PitchClass, Mode)`, `Contains`, `DegreeOf`, `Transpose` | +| `Chord` | Chord parsed from a symbol. | `Parse(string)`, `ChordTones()`, `Voice(octave)`, `Voice(octave, inversion)`, `Transpose` | +| `Key` | Tonic in a mode, resolves function. | `Create(PitchClass, Mode)`, `RomanNumeralOf(Chord)`, `ChordFromRomanNumeral(string)`, `FunctionOf`, `Scale` | +| `Duration` | Exact rational fraction of a whole note. | `Create(int, int)`, presets (`Whole`..`Sixteenth`), `Dotted()`, `Add`, `Multiply`, `AsWholeNotes` | +| `TimeSignature` | Bar and beat lengths. | `Create(int, int)`, `BarDuration`, `BeatDuration` | +| `Note` / `Rest` / `ChordEvent` | Timed musical events (`IMusicalEvent`). | `Create(...)`, `Duration`, `Seconds(Tempo)` | +| `Velocity` | MIDI velocity 0..127. | `Create(int)`, presets (`Piano`..`Fortissimo`) | +| `Tempo` | Beats per minute with a beat unit. | `Create(double)`, `Seconds(Duration)`, `SecondsPerBeat` | + +### Analysis layer + +| Type | Description | Key members | +|------|-------------|-------------| +| `Progression` | Chord sequence with bar-based harmonic rhythm. | `Parse(string)`, `Create(...)`, `RomanNumerals(Key)`, `Functions(Key)`, `Cadences(Key)`, `InferKey()`, `InferKeys()`, `ChromaticChords(Key)` | +| `Section` | Labeled structural unit. | `Create(SectionType, Progression, label?, key?)`, `IsSameStructure(Section)` | +| `Arrangement` | Sections in performance order. | `Create(Key, IEnumerable
)`, `Form`, `TotalBars` | +| `Form` | Structural pattern and its name. | `Of(Arrangement)`, `FromPattern(string)`, `Pattern`, `Name` | +| `HarmonicFunction` | enum: Tonic, Predominant, Dominant, Chromatic. | | +| `CadenceInstance` | A cadence at a resolution index. | `Index`, `Type` (`Cadence` enum: Authentic, Plagal, Half, Deceptive) | +| `ChromaticAnalysis` | A non-diatonic chord classification. | `Index`, `Kind` (`ChromaticKind`), `Detail` | +| `NamedForm` | enum: ThirtyTwoBarAABA, TwelveBarBlues, Binary, Ternary, Rondo, Strophic, ... | | + +`InferKey()` returns `Key?` (null for all-chromatic input), while `InferKeys()` returns the full ranked `IReadOnlyList`. See the [complete library guide](../docs/complete-library-guide.md) for a wider tour. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Paths/README.md b/Semantics.Paths/README.md new file mode 100644 index 0000000..2fc5a14 --- /dev/null +++ b/Semantics.Paths/README.md @@ -0,0 +1,180 @@ +# ktsu.Semantics.Paths + +> Polymorphic, strongly-typed file system path types that keep files, directories, absolute, and relative paths distinct at compile time. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Paths?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Paths) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Paths?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Paths) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Paths?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Paths) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Paths` is one package in the [ktsu.Semantics](../README.md) family. It builds on [`ktsu.Semantics.Strings`](../Semantics.Strings/README.md), which supplies the underlying `SemanticString` machinery. + +## Introduction + +`ktsu.Semantics.Paths` models a file system path as a type, not a bare string. The type encodes two independent facts: whether the path names a file or a directory, and whether it is absolute or relative. That gives concrete types like `AbsoluteFilePath`, `RelativeDirectoryPath`, and orientation-agnostic `FilePath` / `DirectoryPath`, all implementing a shared `IPath` interface hierarchy. You can hold mixed paths in a `List` and filter by capability with `OfType()`. + +Paths are canonicalized on creation (separators normalized, trailing separators trimmed), convert implicitly to `string` so they drop into any `System.IO` API, and compose with a `/` operator that returns the correctly-typed result. + +## Features + +- **Interface hierarchy**: `IPath` with `IFilePath`, `IDirectoryPath`, `IAbsolutePath`, `IRelativePath`, and the four combinations (`IAbsoluteFilePath`, `IAbsoluteDirectoryPath`, `IRelativeFilePath`, `IRelativeDirectoryPath`). +- **Eight concrete path types**: `AbsolutePath`, `RelativePath`, `FilePath`, `DirectoryPath`, `AbsoluteFilePath`, `AbsoluteDirectoryPath`, `RelativeFilePath`, `RelativeDirectoryPath`. +- **Path decomposition**: `FileName`, `FileExtension`, `FullFileExtension` (for `.tar.gz`), `DirectoryPath`, `FileNameWithoutExtension`, plus the `FileName` / `FileExtension` / `DirectoryName` primitive value types. +- **Composition with `/`**: `directory / relativePath`, `directory / FileName`, and similar, each returning the right result type. +- **Absolute/relative conversions**: `AsAbsolute()`, `AsAbsolute(baseDirectory)`, and `AsRelative(baseDirectory)`. +- **Directory navigation**: `Parent`, `Depth`, `IsRoot`, `GetAncestors()`, `IsChildOf`, `IsParentOf`, and strongly-typed `GetContents()` enumeration. +- **Filesystem checks**: `Exists`, `IsFile`, `IsDirectory`. +- **Validation and canonicalization** inherited from the semantic string framework, driven by path attributes such as `[IsAbsolutePath]`, `[IsFileName]`, and `[IsFileExtension]`. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Paths +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Paths +``` + +### Package Reference + +```xml + +``` + +## Usage Examples + +### Basic Example + +```csharp +using ktsu.Semantics.Paths; + +// Build paths with the typed factory, then compose with the '/' operator +AbsoluteDirectoryPath projectDir = AbsoluteDirectoryPath.Create(@"C:\repos\app"); +RelativeFilePath rel = RelativeFilePath.Create(@"src\Program.cs"); + +AbsoluteFilePath source = projectDir / rel; // C:\repos\app\src\Program.cs +FileName name = source.FileName; // Program.cs +FileExtension ext = source.FileExtension; // .cs +AbsoluteDirectoryPath dir = source.AbsoluteDirectoryPath; + +if (source.Exists) +{ + // implicit conversion to string drops straight into System.IO + string text = System.IO.File.ReadAllText(source); +} +``` + +### Absolute and relative conversions + +```csharp +using ktsu.Semantics.Paths; + +AbsoluteDirectoryPath root = AbsoluteDirectoryPath.Create(@"C:\data"); +AbsoluteFilePath file = AbsoluteFilePath.Create(@"C:\data\logs\app.log"); + +RelativeFilePath relative = file.AsRelative(root); // logs\app.log +AbsoluteFilePath backAgain = relative.AsAbsolute(root); // C:\data\logs\app.log + +AbsoluteFilePath renamed = file.ChangeExtension(FileExtension.Create(".bak")); +bool nested = file.IsChildOf(root); // true +``` + +`AsAbsolute()` with no argument resolves a relative path against the current working directory. The `baseDirectory` overload of `AsAbsolute` exists on the relative path types only. + +### Polymorphic collections and typed enumeration + +```csharp +using ktsu.Semantics.Paths; + +List all = +[ + AbsoluteFilePath.Create(@"C:\data.txt"), + RelativeDirectoryPath.Create(@"logs\app"), + FilePath.Create(@"document.pdf"), +]; + +List files = all.OfType().ToList(); +List absolutes = all.OfType().ToList(); + +// GetContents() yields correctly-typed children +AbsoluteDirectoryPath project = AbsoluteDirectoryPath.Create(@"C:\project"); +foreach (IPath entry in project.GetContents()) +{ + switch (entry) + { + case AbsoluteFilePath f: + Console.WriteLine($"file: {f.FileName} ({f.FileExtension})"); + break; + case AbsoluteDirectoryPath d: + Console.WriteLine($"dir: {d.Name} at depth {d.Depth}"); + break; + } +} +``` + +`GetContents()` returns an empty sequence rather than throwing when the directory is missing or access is denied. + +## API Reference + +### Interface hierarchy + +| Interface | Extends | Notable member | +|-----------|---------|----------------| +| `IPath` | (none) | marker | +| `IFilePath` | `IPath` | `AbsoluteFilePath AsAbsolute()` | +| `IDirectoryPath` | `IPath` | `AsAbsolute()`, `IEnumerable GetContents()` | +| `IAbsolutePath` | `IPath` | `AbsolutePath AsAbsolute()` | +| `IRelativePath` | `IPath` | `AbsolutePath AsAbsolute()` | +| `IAbsoluteFilePath` | `IFilePath, IAbsolutePath` | typed `AsAbsolute()` | +| `IAbsoluteDirectoryPath` | `IDirectoryPath, IAbsolutePath` | typed `AsAbsolute()` | +| `IRelativeFilePath` | `IFilePath, IRelativePath` | typed `AsAbsolute()` | +| `IRelativeDirectoryPath` | `IDirectoryPath, IRelativePath` | typed `AsAbsolute()` | + +### Concrete types and creation + +All concrete types are `sealed record`s created through the inherited static factory `Create(...)` (accepting `string`, `char[]`, or `ReadOnlySpan`). `Create` throws `ArgumentException` on invalid input and `ArgumentNullException` on null. + +| Type | Kind | Interface | +|------|------|-----------| +| `AbsolutePath` | untyped absolute | `IAbsolutePath` | +| `RelativePath` | untyped relative | `IRelativePath` | +| `FilePath` | orientation-agnostic file | `IFilePath` | +| `DirectoryPath` | orientation-agnostic directory | `IDirectoryPath` | +| `AbsoluteFilePath` | absolute file | `IAbsoluteFilePath` | +| `AbsoluteDirectoryPath` | absolute directory | `IAbsoluteDirectoryPath` | +| `RelativeFilePath` | relative file | `IRelativeFilePath` | +| `RelativeDirectoryPath` | relative directory | `IRelativeDirectoryPath` | + +The primitive component types `FileName`, `FileExtension`, and `DirectoryName` are semantic strings in their own right. + +### Common members + +| Name | Type | Description | +|------|------|-------------| +| `Exists` | `bool` | True if the path is an existing file or directory. | +| `IsFile` / `IsDirectory` | `bool` | Filesystem-backed checks. | +| `FileName` | `FileName` | Filename portion (file paths). | +| `FileExtension` / `FullFileExtension` | `FileExtension` | Last extension / everything from the first dot. | +| `DirectoryPath` | `DirectoryPath` | Directory portion of a file path. | +| `Parent` / `Name` / `Depth` / `IsRoot` | directory members | Directory navigation. | +| `AsAbsolute()` | typed absolute path | Resolve against the current working directory. | +| `AsAbsolute(AbsoluteDirectoryPath)` | typed absolute path | Resolve a relative path against a base (relative types only). | +| `AsRelative(AbsoluteDirectoryPath)` | typed relative path | Make relative to a base directory. | +| `GetContents()` | `IEnumerable` | Strongly-typed directory children. | +| `operator /` | typed result | Combine a directory with a relative path, `FileName`, or `DirectoryName`. | + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Quantities.Decimal/README.md b/Semantics.Quantities.Decimal/README.md new file mode 100644 index 0000000..c96a697 --- /dev/null +++ b/Semantics.Quantities.Decimal/README.md @@ -0,0 +1,80 @@ +# ktsu.Semantics.Quantities.Decimal + +> Storage-type aliases that bind every `ktsu.Semantics.Quantities` type to `decimal`, so you write `Mass` instead of `Mass`, project-wide. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Quantities.Decimal?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Decimal) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Quantities.Decimal?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Decimal) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Quantities.Decimal?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Decimal) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Quantities.Decimal` is a satellite of [`ktsu.Semantics.Quantities`](../Semantics.Quantities/README.md) in the [ktsu.Semantics](../README.md) family. Read the core package README first for the quantity API itself. + +## Introduction + +Every quantity in `ktsu.Semantics.Quantities` is generic over its numeric storage type, so you normally write `Mass`, `Speed`, and so on. If a project uses one storage type throughout, that generic argument is noise. + +This package is props-only. It ships no assembly, just a `buildTransitive` props file that injects one C# global-using alias per quantity, binding each open generic type to `decimal`. Reference it and you can write `Mass`, `Speed`, `Force3D` with no generic argument, and every quantity resolves to its `decimal` form. The aliases are real `Mass` (and so on), so they interoperate with the entire API with no conversion. + +Installing this package also pulls in the matching version of `ktsu.Semantics.Quantities` as a dependency, so it is the only reference you need. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Quantities.Decimal +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Quantities.Decimal +``` + +### Package Reference + +```xml + +``` + +## Usage Example + +```csharp +using ktsu.Semantics.Quantities; // types resolve to their decimal form via the injected aliases + +Mass mass = Mass.FromKilogram(10.0m); +Speed speed = Speed.FromMeterPerSecond(15.0m); +Mass total = mass + Mass.FromKilogram(2.0m); // still a Mass, full identity + +Force3D f = new() { X = 3.0m, Y = 4.0m, Z = 0.0m }; +ForceMagnitude mag = f.Magnitude(); // 5.0 +``` + +Under the hood the injected alias looks like this (one line per quantity, roughly 220 in total): + +```xml + +``` + +## Use exactly one storage-type alias package per project + +The aliases are project-wide global usings keyed on the bare type name (`Mass`, `Speed`, ...). Referencing two flavor packages in the same project would define the same alias name twice (one bound to `decimal`, one to another type), which is a compile error. So reference exactly one of: + +- [`ktsu.Semantics.Quantities.Double`](../Semantics.Quantities.Double/README.md) +- [`ktsu.Semantics.Quantities.Float`](../Semantics.Quantities.Float/README.md) +- `ktsu.Semantics.Quantities.Decimal` (this package) + +A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. + +The alias lists are generated from the quantity catalogue by `scripts/Generate-AliasProps.ps1` and validated in CI, so they stay in lockstep with the quantities the core package emits. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Quantities.Double/README.md b/Semantics.Quantities.Double/README.md new file mode 100644 index 0000000..73e76f2 --- /dev/null +++ b/Semantics.Quantities.Double/README.md @@ -0,0 +1,80 @@ +# ktsu.Semantics.Quantities.Double + +> Storage-type aliases that bind every `ktsu.Semantics.Quantities` type to `double`, so you write `Mass` instead of `Mass`, project-wide. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Quantities.Double?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Double) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Quantities.Double?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Double) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Quantities.Double?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Double) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Quantities.Double` is a satellite of [`ktsu.Semantics.Quantities`](../Semantics.Quantities/README.md) in the [ktsu.Semantics](../README.md) family. Read the core package README first for the quantity API itself. + +## Introduction + +Every quantity in `ktsu.Semantics.Quantities` is generic over its numeric storage type, so you normally write `Mass`, `Speed`, and so on. If a project uses one storage type throughout, that generic argument is noise. + +This package is props-only. It ships no assembly, just a `buildTransitive` props file that injects one C# global-using alias per quantity, binding each open generic type to `double`. Reference it and you can write `Mass`, `Speed`, `Force3D` with no generic argument, and every quantity resolves to its `double` form. The aliases are real `Mass` (and so on), so they interoperate with the entire API with no conversion. + +Installing this package also pulls in the matching version of `ktsu.Semantics.Quantities` as a dependency, so it is the only reference you need. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Quantities.Double +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Quantities.Double +``` + +### Package Reference + +```xml + +``` + +## Usage Example + +```csharp +using ktsu.Semantics.Quantities; // types resolve to their double form via the injected aliases + +Mass mass = Mass.FromKilogram(10.0); +Speed speed = Speed.FromMeterPerSecond(15.0); +Mass total = mass + Mass.FromKilogram(2.0); // still a Mass, full identity + +Force3D f = new() { X = 3.0, Y = 4.0, Z = 0.0 }; +ForceMagnitude mag = f.Magnitude(); // 5.0 +``` + +Under the hood the injected alias looks like this (one line per quantity, roughly 220 in total): + +```xml + +``` + +## Use exactly one storage-type alias package per project + +The aliases are project-wide global usings keyed on the bare type name (`Mass`, `Speed`, ...). Referencing two flavor packages in the same project would define the same alias name twice (one bound to `double`, one to another type), which is a compile error. So reference exactly one of: + +- `ktsu.Semantics.Quantities.Double` (this package) +- [`ktsu.Semantics.Quantities.Float`](../Semantics.Quantities.Float/README.md) +- [`ktsu.Semantics.Quantities.Decimal`](../Semantics.Quantities.Decimal/README.md) + +A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. + +The alias lists are generated from the quantity catalogue by `scripts/Generate-AliasProps.ps1` and validated in CI, so they stay in lockstep with the quantities the core package emits. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Quantities.Float/README.md b/Semantics.Quantities.Float/README.md new file mode 100644 index 0000000..a90fdfc --- /dev/null +++ b/Semantics.Quantities.Float/README.md @@ -0,0 +1,80 @@ +# ktsu.Semantics.Quantities.Float + +> Storage-type aliases that bind every `ktsu.Semantics.Quantities` type to `float`, so you write `Mass` instead of `Mass`, project-wide. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Quantities.Float?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Float) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Quantities.Float?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Float) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Quantities.Float?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Float) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Quantities.Float` is a satellite of [`ktsu.Semantics.Quantities`](../Semantics.Quantities/README.md) in the [ktsu.Semantics](../README.md) family. Read the core package README first for the quantity API itself. + +## Introduction + +Every quantity in `ktsu.Semantics.Quantities` is generic over its numeric storage type, so you normally write `Mass`, `Speed`, and so on. If a project uses one storage type throughout, that generic argument is noise. + +This package is props-only. It ships no assembly, just a `buildTransitive` props file that injects one C# global-using alias per quantity, binding each open generic type to `float`. Reference it and you can write `Mass`, `Speed`, `Force3D` with no generic argument, and every quantity resolves to its `float` form. The aliases are real `Mass` (and so on), so they interoperate with the entire API with no conversion. + +Installing this package also pulls in the matching version of `ktsu.Semantics.Quantities` as a dependency, so it is the only reference you need. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Quantities.Float +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Quantities.Float +``` + +### Package Reference + +```xml + +``` + +## Usage Example + +```csharp +using ktsu.Semantics.Quantities; // types resolve to their float form via the injected aliases + +Mass mass = Mass.FromKilogram(10.0f); +Speed speed = Speed.FromMeterPerSecond(15.0f); +Mass total = mass + Mass.FromKilogram(2.0f); // still a Mass, full identity + +Force3D f = new() { X = 3.0f, Y = 4.0f, Z = 0.0f }; +ForceMagnitude mag = f.Magnitude(); // 5.0 +``` + +Under the hood the injected alias looks like this (one line per quantity, roughly 220 in total): + +```xml + +``` + +## Use exactly one storage-type alias package per project + +The aliases are project-wide global usings keyed on the bare type name (`Mass`, `Speed`, ...). Referencing two flavor packages in the same project would define the same alias name twice (one bound to `float`, one to another type), which is a compile error. So reference exactly one of: + +- [`ktsu.Semantics.Quantities.Double`](../Semantics.Quantities.Double/README.md) +- `ktsu.Semantics.Quantities.Float` (this package) +- [`ktsu.Semantics.Quantities.Decimal`](../Semantics.Quantities.Decimal/README.md) + +A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. + +The alias lists are generated from the quantity catalogue by `scripts/Generate-AliasProps.ps1` and validated in CI, so they stay in lockstep with the quantities the core package emits. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Quantities/README.md b/Semantics.Quantities/README.md new file mode 100644 index 0000000..e8ca379 --- /dev/null +++ b/Semantics.Quantities/README.md @@ -0,0 +1,165 @@ +# ktsu.Semantics.Quantities + +> A metadata-generated, type-safe physical quantity system built on a unified vector model, with compile-time dimensional analysis, generated unit conversions and physics operators, and centralized physical constants. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Quantities?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Quantities?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Quantities?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Quantities` is one package in the [ktsu.Semantics](../README.md) family. If you use a single numeric storage type throughout a project, the alias packages [`ktsu.Semantics.Quantities.Double`](../Semantics.Quantities.Double/README.md), [`.Float`](../Semantics.Quantities.Float/README.md), and [`.Decimal`](../Semantics.Quantities.Decimal/README.md) let you drop the generic argument and write `Mass` instead of `Mass`. + +## Introduction + +`ktsu.Semantics.Quantities` gives you physical quantities as types, so a `Force` cannot be added to a `Speed` and multiplying a `Mass` by an `AccelerationMagnitude` yields a `ForceMagnitude` at compile time. Every quantity is generic over its numeric storage type (`Mass`, `Speed`, `Length`), and all values are stored in SI base units. + +The type surface is generated. A single source of truth, `dimensions.json`, drives a Roslyn incremental generator that emits the quantity records, their `From{Unit}` factories with built-in conversions, the cross-dimensional physics operators, and the physical constants. That is roughly 72 physical dimensions and over 200 generated quantity types, all committed to source so the project compiles without first running the generator. + +Every quantity is a vector, and the dimensionality of its direction space is part of the type. + +| Form | Meaning | Sign | Examples | +|------|---------|------|----------| +| `IVector0` | magnitude only | always `>= 0` | `Speed`, `Mass`, `Energy`, `Distance`, `Area` | +| `IVector1` | signed 1D | signed | `Velocity1D`, `Force1D`, `Temperature` | +| `IVector2` | 2D directional | per-component | `Velocity2D`, `Force2D`, `Acceleration2D` | +| `IVector3` | 3D directional | per-component | `Velocity3D`, `Force3D`, `Position3D` | +| `IVector4` | 4D directional | per-component | reserved (relativistic / spacetime) | + +`IVectorN.Magnitude()` (for N >= 1) returns the matching `IVector0` quantity. + +## Features + +- **Compile-time dimensional safety**: illegal combinations do not compile, and cross-dimensional operators produce the correct result type. +- **Generated unit conversions**: `From{Unit}` factories per declared unit (`Mass.FromKilogram`, `Speed.FromMeterPerSecond`, `Length.FromFoot`), converting to the SI base unit on construction, with `In(unit)` to convert back. +- **Unified vector model**: `IVector0` through `IVector4`, with `Magnitude()`, `Dot`, `Cross`, `Normalize`, and typed cross-quantity results (`Force3D.Dot(Displacement3D)` returns `Energy`). +- **Construction-time invariants**: `IVector0` magnitudes are guarded non-negative, and quantities where zero is unphysical (`Wavelength`, `Period`, `HalfLife`) are guarded strictly positive. +- **Physical constants**: `PhysicalConstants` with domain-grouped `PreciseNumber` values and generic accessors that materialize into any `T : INumber`. +- **Generator diagnostics**: metadata problems (`SEM001`-`SEM005`) are caught at build time. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Quantities +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Quantities +``` + +### Package Reference + +```xml + +``` + +For a project that uses one storage type everywhere, reference an alias package instead (or in addition) so you can omit the generic argument. See [storage-type aliases](#storage-type-aliases). + +## Usage Examples + +### Basic Example: magnitudes and operators + +```csharp +using ktsu.Semantics.Quantities; + +Mass m = Mass.FromKilogram(2.0); +Speed v = Speed.FromMeterPerSecond(3.0); + +MomentumMagnitude p = m * v; // Mass * Speed -> MomentumMagnitude +double kg = m.Value; // stored SI-base value + +// arithmetic and comparison are inherited +Speed faster = v + Speed.FromMeterPerSecond(5.0); +bool ok = faster > v; + +// construction-time guard: a negative magnitude throws ArgumentException +// Speed.FromMeterPerSecond(-1.0); +``` + +### Vector quantities + +```csharp +using ktsu.Semantics.Quantities; + +Force3D f = new() { X = 3.0, Y = 4.0, Z = 0.0 }; +ForceMagnitude mag = f.Magnitude(); // matching Vector0 quantity, value 5.0 +Force3D unit = f.Normalize(); + +Displacement3D d = new() { X = 1.0, Y = 0.0, Z = 0.0 }; +Energy work = f.Dot(d); // typed dot: Force . Displacement = Energy +Torque3D torque = f.Cross(d); // typed cross: Force x Displacement = Torque +Momentum3D impulse = f * Duration.Create(2.0); +``` + +### Physical constants + +```csharp +using ktsu.Semantics.Quantities; + +// domain-grouped, exact PreciseNumber values +var c = PhysicalConstants.Fundamental.SpeedOfLight; // 299_792_458 m/s +var R = PhysicalConstants.Chemistry.GasConstant; // 8.31446... J/(mol.K) + +// generic accessors materialize into any T : INumber +double g = PhysicalConstants.Generic.StandardGravity(); // 9.80665 +ForceMagnitude weight = Mass.FromKilogram(70.0) * AccelerationMagnitude.Create(g); +``` + +## API Reference + +### Hand-written runtime types + +| Type | Description | +|------|-------------| +| `SemanticQuantity` | Arithmetic base. `Create(T)` factory, `Quantity` value, and inherited `+ - * / -` operators. Divide by zero throws `DivideByZeroException`. | +| `PhysicalQuantity` | Abstract base for scalar quantities. Adds `Value`, `Dimension`, `IsPhysicallyValid`, comparison operators, and cross-dimension-aware `CompareTo`/`Equals`. | +| `IVector0` | Magnitude-only marker: `Value`, static `Zero`. Non-negative by construction. | +| `IVector1` | Signed single-axis: `Value`, static `Zero`. | +| `IVector2` / `IVector3` / `IVector4` | Directional vectors with `X`/`Y`/`Z`/`W`, `Length()`, `LengthSquared()`, `Dot`, `Distance`, `Normalize`; `IVector3` adds `Cross`. | +| `Vector0Guards` | `EnsureNonNegative(value, name)` and `EnsurePositive(value, name)`, used by generated `From{Unit}` factories. | +| `UnitSystem` | enum classifying units (`SIBase`, `SIDerived`, `Metric`, `Imperial`, ...). | + +### Generated quantity types + +Each generated quantity is a `partial record Name where T : struct, INumber`. A typical `IVector0` quantity such as `Mass` exposes: + +| Member | Description | +|--------|-------------| +| `From{Unit}(T)` | e.g. `FromKilogram`, `FromGram`, `FromPound`. Converts to SI base and applies the guard. | +| `Create(T)` | Inherited base factory (value already in SI base units). | +| `Value` | The stored SI-base value. | +| `In(unit)` | Convert the value back to a specific unit. | +| typed operators | e.g. `Mass * AccelerationMagnitude -> ForceMagnitude`, `Mass / Volume -> Density`. | + +Vector quantities (`Force3D`, `Velocity2D`, ...) implement the matching `IVectorN` and add `Magnitude()` (returning the corresponding `*Magnitude` Vector0 quantity), `Dot`, `Cross`, and typed cross-quantity results. + +Factory names use the singular lemma of each unit name verbatim (`FromMeterPerSecond`, `FromRevolutionPerMinute`). There is no pluralization step. + +### `PhysicalConstants` + +- **Domain-grouped**: nested static classes returning `PreciseNumber`, for example `PhysicalConstants.Fundamental.SpeedOfLight`, `PhysicalConstants.ClassicalMechanics.StandardGravity`, `PhysicalConstants.Thermodynamics.WaterTriplePoint`. Domain groups include Acoustics, AngularMechanics, Chemistry, ClassicalMechanics, FluidMechanics, Fundamental, NuclearPhysics, Optics, and Thermodynamics. +- **Generic**: `PhysicalConstants.Generic.Name()` materializes any constant into a `T : INumber`. + +## Architecture + +The system is metadata-driven. The single source of truth is `Semantics.SourceGenerators/Metadata/dimensions.json` (alongside `units.json`, `magnitudes.json`, `conversions.json`, `domains.json`, and `logarithmic.json`). A Roslyn incremental generator emits one record per quantity, a `From{Unit}` factory per declared unit, the cross-dimensional `*` / `/` / `Dot` / `Cross` operators declared in the metadata, and the `PhysicalConstants` surface. Logarithmic-scale quantities (decibels, cents, pH) are generated separately because they do not obey linear arithmetic. + +Generated output is committed to `Generated/`, so the project compiles without first running the generator. For the full design and an end-to-end "add a dimension" walk-through, see: + +- [Unified vector model strategy](../docs/strategy-unified-vector-quantities.md) +- [Source-generator workflow and schema](../docs/physics-generator.md) +- [Physics domains tour](../docs/physics-domains-guide.md) + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Strings.Identifiers/README.md b/Semantics.Strings.Identifiers/README.md new file mode 100644 index 0000000..76981ba --- /dev/null +++ b/Semantics.Strings.Identifiers/README.md @@ -0,0 +1,101 @@ +# ktsu.Semantics.Strings.Identifiers + +> Ready-made, self-validating identifier string types (`Uuid`, `Ulid`, `Iban`, `Isbn`, `CreditCardNumber`, `JwtToken`) built on the Semantics.Strings framework. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Strings.Identifiers?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Strings.Identifiers) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Strings.Identifiers?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Strings.Identifiers) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Strings.Identifiers?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Strings.Identifiers) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Strings.Identifiers` is one package in the [ktsu.Semantics](../README.md) family. It builds on [`ktsu.Semantics.Strings`](../Semantics.Strings/README.md), which you should read first for the underlying `SemanticString` model. + +## Introduction + +Defining your own semantic string types is the core use of the framework, but a handful of identifier formats show up in almost every codebase. `ktsu.Semantics.Strings.Identifiers` ships six of them ready to use, each with real normalization and real check-digit or structural validation. You get compile-time-distinct types with no boilerplate. + +Validation is pragmatic rather than exhaustive, with documented limits. For example `JwtToken` verifies the three-segment structure and that the header and payload decode to JSON objects, but it does **not** verify the signature. + +## Features + +- **Six identifier types**: `Uuid`, `Ulid`, `Iban`, `Isbn`, `CreditCardNumber`, `JwtToken`. +- **Normalization on creation**: whitespace and separators stripped, casing folded to the canonical form for each format. +- **Real validation**: check-digit maths for `Iban` (ISO 7064 mod-97), `Isbn` (mod-11 / mod-10), and `CreditCardNumber` (Luhn), structural and format rules for `Uuid`, `Ulid`, and `JwtToken`. +- **Same surface as any semantic string**: `Create`, `TryCreate`, `As()`, value equality, ordering, implicit `string` conversion, and JSON round-tripping, all inherited from `SemanticString`. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Strings.Identifiers +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Strings.Identifiers +``` + +### Package Reference + +```xml + +``` + +## Usage Examples + +### Basic Example + +```csharp +using ktsu.Semantics.Strings.Identifiers; + +Uuid id = Uuid.Create("{123E4567-E89B-12D3-A456-426614174000}"); // -> "123e4567-e89b-12d3-a456-426614174000" +Ulid ulid = Ulid.Create("01ARZ3NDEKTSV4RRFFQ69G5FAV"); +Isbn isbn = Isbn.Create("978-0-306-40615-7"); // ISBN-10 or ISBN-13, check-digit validated +Iban iban = Iban.Create("GB82 WEST 1234 5698 7654 32"); // whitespace stripped, mod-97 validated + +Uuid.Create("not-a-uuid"); // throws ArgumentException +``` + +### Safe creation and normalization + +```csharp +using ktsu.Semantics.Strings.Identifiers; + +// TryCreate never throws +if (CreditCardNumber.TryCreate("4111 1111 1111 1111", out CreditCardNumber? card)) +{ + // separators stripped, Luhn-checked; card.WeakString == "4111111111111111" +} + +// The .As() extension works too +CreditCardNumber same = "4111-1111-1111-1111".As(); +``` + +Every type inherits the full `SemanticString` factory surface (`Create`, `TryCreate`, the `char[]` and `ReadOnlySpan` overloads, `As()`), so anything shown in the [Semantics.Strings README](../Semantics.Strings/README.md) applies here as well. + +## API Reference + +Each type is a `sealed record` deriving from `SemanticString`. None declare their own `Create`/`TryCreate`, they inherit them. The table describes what each type normalizes and validates. + +| Type | Normalization | Validation | Documented limits | +|------|---------------|------------|-------------------| +| `Uuid` | Trim, strip wrapping `{}` / `()`, lowercase | Canonical 8-4-4-4-12 hex (RFC 4122 layout) | Any variant/version accepted, including the nil UUID. Not version-checked. | +| `Ulid` | Trim, uppercase | 26 Crockford base32 chars, first char `0`-`7` | Timestamp is not otherwise decoded. | +| `Iban` | Strip spaces, uppercase | Length 15-34, country/check prefix, ISO 7064 mod-97-10 checksum | Country-specific BBAN structure is not enforced. | +| `Isbn` | Strip `-` and spaces, uppercase | ISBN-10 (weighted mod-11, `X` allowed) or ISBN-13 (mod-10) | Registration-group/publisher ranges are not validated. | +| `CreditCardNumber` | Strip spaces and hyphens | 13-19 digits, Luhn (mod-10) checksum | Luhn only. No issuer/network detection, no PCI guarantee. The value is sensitive, do not log it. | +| `JwtToken` | None (stored verbatim, case-sensitive) | Three `.`-separated segments, non-empty header and payload that decode to JSON objects | Signature is not decoded or verified. `alg`, claims, and expiry are not inspected. | + +Creation follows the base-type contract: `Create(...)` throws `ArgumentException` on invalid input and `ArgumentNullException` on null, while `TryCreate(...)` returns `false` instead. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Strings/README.md b/Semantics.Strings/README.md new file mode 100644 index 0000000..16e89fa --- /dev/null +++ b/Semantics.Strings/README.md @@ -0,0 +1,186 @@ +# ktsu.Semantics.Strings + +> Strongly-typed, self-validating string wrappers that replace primitive obsession with compile-time-safe domain types. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Strings?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Strings) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Strings?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Strings) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Strings?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Strings) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Strings` is one package in the [ktsu.Semantics](../README.md) family. For the family overview and the other pillars (paths, quantities, music, color) start at the [root README](../README.md). + +## Introduction + +`ktsu.Semantics.Strings` gives you a base type, `SemanticString`, for defining string-shaped domain types such as `EmailAddress`, `UserId`, or `BlogSlug`. A semantic string validates itself on construction, normalizes its value, carries the whole `System.String` surface, and is distinct from every other semantic type at compile time. An `EmailAddress` will not silently substitute for a `UserId`, so a whole class of "passed the arguments in the wrong order" bugs stops compiling. + +Validation is declarative. You attach attributes such as `[IsEmailAddress]` or `[StartsWith("USER_")]` to the type, and the framework runs them whenever an instance is created. Ready-made identifier types (`Uuid`, `Iban`, `Isbn`, and more) live in the companion [`ktsu.Semantics.Strings.Identifiers`](../Semantics.Strings.Identifiers/README.md) package. + +## Features + +- **`SemanticString` base type**: an abstract record using the curiously-recurring template pattern, so derived types get value equality, ordering, and the full string API for free. +- **Validating factories**: `Create` throws on invalid input, `TryCreate` returns a bool and never throws. Both accept `string`, `char[]`, and `ReadOnlySpan`. +- **Declarative validation attributes**: casing, format, text, and first-class .NET type checks, combined with `[ValidateAll]` (default, logical AND) or `[ValidateAny]` (logical OR). +- **Normalization hook**: override `MakeCanonical` to trim, case-fold, or otherwise canonicalize a value before validation runs. +- **Fluent conversions**: `"user@example.com".As()` and cross-type reinterpretation via `source.As()`. +- **Factory abstraction for dependency injection**: `ISemanticStringFactory` / `SemanticStringFactory` for constructor injection, with a `SemanticStringFactory.Default` singleton for non-DI use. +- **Span-friendly and allocation-conscious**: span-based overloads and a `ref struct` split enumerator on the target frameworks that support them. +- **JSON round-trip serialization**: values serialize as their underlying string via `ktsu.RoundTripStringJsonConverter`. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Strings +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Strings +``` + +### Package Reference + +```xml + +``` + +## Usage Examples + +### Basic Example + +```csharp +using ktsu.Semantics.Strings; + +[IsEmailAddress] +public sealed record EmailAddress : SemanticString { } + +[StartsWith("USER_"), HasNonWhitespaceContent] +public sealed record UserId : SemanticString { } + +// Direct construction, no generic argument needed +EmailAddress email = EmailAddress.Create("user@example.com"); +UserId userId = UserId.Create("USER_12345"); + +// Safe creation, no exception on failure +if (EmailAddress.TryCreate("maybe@invalid", out EmailAddress? safe)) +{ + // use safe +} + +// Compile-time safety +public void SendWelcomeEmail(EmailAddress to, UserId who) { /* ... */ } +// SendWelcomeEmail(userId, email); // does not compile +``` + +A semantic string converts implicitly to `string`, so it drops into any API that expects one. Construction is always explicit (`Create` / `As`), which guarantees validation runs. + +### Combining attributes + +```csharp +// All attributes must pass (default behavior) +[IsEmailAddress, EndsWith(".com")] +public sealed record DotComEmail : SemanticString { } + +// Any one attribute passing is sufficient +[ValidateAny] +[IsEmailAddress, IsUri] +public sealed record ContactMethod : SemanticString { } +``` + +The parameterized text attributes (`Contains`, `StartsWith`, `EndsWith`, `PrefixAndSuffix`, `RegexMatch`) allow multiples, so you can stack several and combine them with `[ValidateAny]`. + +### Normalization before validation + +```csharp +using ktsu.Semantics.Strings; + +[HasNonWhitespaceContent] +public sealed record Slug : SemanticString +{ + protected override string MakeCanonical(string input) => + input.Trim().ToLowerInvariant().Replace(' ', '-'); +} + +Slug slug = Slug.Create(" Hello World "); // stored as "hello-world" +``` + +### Dependency injection + +The package ships no `AddSemanticStrings()` helper. Register each closed factory type you need: + +```csharp +services.AddScoped, SemanticStringFactory>(); + +public class UserService(ISemanticStringFactory emails) +{ + public User CreateUser(string raw) => + emails.TryFromString(raw, out EmailAddress? email) + ? new User(email!) + : throw new ArgumentException("invalid email"); +} +``` + +For code that is not using a container, `SemanticStringFactory.Default` is a ready singleton. + +## API Reference + +### `SemanticString` + +Abstract base record for all semantic string types. `TDerived` is the concrete type itself. + +#### Key members + +| Name | Signature | Description | +|------|-----------|-------------| +| `WeakString` | `string { get; init; }` | The underlying raw value. | +| `Length` | `int { get; }` | Length of the underlying string. | +| `Create` | `static TDerived Create(string?)` (also `char[]`, `ReadOnlySpan`) | Validates and constructs. Throws `ArgumentException` on invalid input, `ArgumentNullException` on null. | +| `TryCreate` | `static bool TryCreate(string?, out TDerived?)` (also `char[]`, `ReadOnlySpan`) | Returns `false` instead of throwing. | +| `As()` | `TDest As()` | Reinterprets the value as another semantic type, re-validating against its rules. | +| `MakeCanonical` | `protected virtual string MakeCanonical(string)` | Normalization hook run before validation. | +| `IsValid` | `virtual bool IsValid()` | True when the value is non-null and passes attribute validation. | +| `WithPrefix` / `WithSuffix` | `TDerived WithPrefix(string)` / `TDerived WithSuffix(string)` | Type-safe prefix/suffix transforms. | +| implicit `string` | `implicit operator string(SemanticString?)` | Converts to `string` (null becomes `string.Empty`). | +| `<`, `<=`, `>`, `>=`, `CompareTo` | ordering members | Ordinal comparison on the underlying string. | + +The base also forwards the common `System.String` surface (`Contains`, `IndexOf`, `Substring`, `Split`, `Trim`, `StartsWith`, `EndsWith`, casing helpers, and more) plus span-based helpers on the target frameworks that support them. + +### `ISemanticStringFactory` / `SemanticStringFactory` + +| Name | Return Type | Description | +|------|-------------|-------------| +| `FromString(string?)` | `T` | Creates an instance, throwing on invalid input. | +| `FromCharArray(char[]?)` | `T` | As above from a char array. | +| `TryFromString(string?, out T?)` | `bool` | Non-throwing creation. | +| `SemanticStringFactory.Default` | `SemanticStringFactory` | Shared singleton for non-DI use. | + +### Validation attributes + +All attributes apply to a class, derive from `SemanticStringValidationAttribute`, and live in the `ktsu.Semantics.Strings` namespace. + +| Category | Representative attributes | +|----------|---------------------------| +| Casing | `IsCamelCase`, `IsPascalCase`, `IsSnakeCase`, `IsKebabCase`, `IsMacroCase`, `IsTitleCase`, `IsSentenceCase`, `IsUpperCase`, `IsLowerCase` | +| Format | `HasNonWhitespaceContent`, `IsSingleLine`, `IsMultiLine`, `HasMinimumLines(n)`, `HasMaximumLines(n)`, `HasExactLines(n)`, `IsEmptyOrWhitespace` | +| Text | `Contains(substring)`, `StartsWith(prefix)`, `EndsWith(suffix)`, `PrefixAndSuffix(prefix, suffix)`, `RegexMatch(pattern)`, `IsBase64`, `IsEmailAddress` | +| First-class .NET types | `IsBoolean`, `IsDateTime`, `IsDecimal`, `IsDouble`, `IsInt32`, `IsGuid`, `IsUri`, `IsVersion`, `IsTimeSpan`, `IsIpAddress` | +| Combination markers | `[ValidateAll]` (default), `[ValidateAny]` | + +The full catalogue lives in the [validation reference](../docs/validation-reference.md). + +## Architecture + +Validation is a small strategy/adapter/rule pipeline. A combination **strategy** (`ValidateAllStrategy` / `ValidateAnyStrategy`, chosen by `ValidationStrategyFactory`) decides whether a type's attributes are combined with AND or OR. Each attribute delegates to a **`ValidationAdapter`** that returns a `ValidationResult`. A separate **rule** abstraction (`IValidationRule`, `ValidationRuleBase`) provides an open extension point for adding named, prioritized rules without touching existing code. See the [architecture guide](../docs/architecture.md) for the full picture. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details.