Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
id-token: write # enable GitHub OIDC token issuance for this job
strategy:
matrix:
dotnet-version: [8.x, 9.x] # test all supported versions
dotnet-version: [8.x, 9.x, 10.x] # test all supported versions
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -34,18 +34,18 @@ jobs:
run: dotnet test --configuration Release --no-build --verbosity normal -maxcpucount:1

- name: Pack
if: matrix.dotnet-version == '9.x'
if: matrix.dotnet-version == '10.x'
run: dotnet pack --configuration Release --no-build --output ./nupkg

- name: Nuget Login (OIDC + temp API Key)
if: matrix.dotnet-version == '9.x'
- name: Nuget Login (OIDC + temp API Key)
if: matrix.dotnet-version == '10.x'
uses: Nuget/login@v1
id: login
with:
user: ${{secrets.NUGET_USER}}

- name: Nuget push
if: matrix.dotnet-version == '9.x'
if: matrix.dotnet-version == '10.x'
run: dotnet nuget push nupkg/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json

- name: Done
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,7 @@ paket-files/

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
*.pyc

# Claude
.claude/
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AGENTS.md

See [CLAUDE.md](./CLAUDE.md) for all agent guidance for this repository.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## [4.0.0] - 2026-02-10

### Added
- **New String extensions:** `Reverse`, `ContainsIgnoreCase`, `Left`, `Right`, `WordCount`, `EnsureStartsWith`, `EnsureEndsWith`, `FromShortGuid`
- **New DateTime extensions:** `IsWeekday`, `IsBetween`, `ToUnixTimestamp`, `StartOfMonth`, `EndOfMonth`, `StartOfWeek`, `DaysInMonth`
- **New Integer extensions:** `IsBetween`, `IsPositive`, `IsNegative`
- **New Decimal extensions:** `IsPositive`, `IsNegative`
- .NET 10 support — the project now targets .NET 8, .NET 9, and .NET 10

### Changed
- Updated CI/CD pipeline to build, test, and publish from .NET 10

### Fixed
- `IsEmail` and `IsJson` parameter type changed from `string` to `string?` for consistency
- `IsInteger` now trims input before parsing, consistent with `ParseToInt`
- `ToKebabCase` now uses pre-compiled/source-generated regex instead of inline `Regex.Replace`
- `FromBase64` now catches `FormatException` specifically instead of bare `catch`
- `ToSHA256Hash` null check changed from `IsNullOrWhiteSpace` to `IsNullOrEmpty` for consistency with `ToMD5Hash`

### Breaking Changes
- **`ToSHA256Hash`** now returns lowercase hexadecimal (matching `ToMD5Hash`) instead of Base64
- **`ToCurrency`** default culture changed from `CurrentCulture` to `InvariantCulture` for consistency with `ToMoneyFormat`

---

## [3.0.0] - YYYY-MM-DD

### Added
Expand Down
57 changes: 57 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

OpenCode is a .NET utility library providing extension methods for common types (string, bool, int, decimal, DateTime, Guid). It is published as a NuGet package targeting .NET 8, .NET 9, and .NET 10.

## Build & Test Commands

```bash
# Build
dotnet build
dotnet build --configuration Release

# Run all tests
dotnet test

# Run tests for a specific framework
dotnet test --framework net9.0

# Run a single test class
dotnet test --filter "FullyQualifiedName~OpenCode.Tests.StringExtension.StringExtensionCoreTests"

# Run a single test method
dotnet test --filter "FullyQualifiedName~MethodName"

# Pack NuGet package
dotnet pack --configuration Release --output ./nupkg
```

No separate lint command — code style is enforced via `.editorconfig`.

## Architecture

The solution has two projects:

- **OpenCode/** — The library. Each type's extensions live in a subfolder (e.g., `StringExtension/`, `IntegerExtension/`). StringExtension is split across multiple files using `partial class` (e.g., `StringExtension.Core.cs`, `StringExtension.Hash.cs`, `StringExtension.Bool.cs`).
- **OpenCode.Tests/** — xUnit tests mirroring the source folder structure 1:1.

All extension classes are `public static` in the `namespace OpenCode;` (file-scoped). StringExtension classes are `partial`; other type extensions are `sealed`.

## Key Conventions

- **Null safety**: Extension methods handle null inputs gracefully (return defaults, empty strings, etc.) — they never throw on null. Nullable reference types are enabled.
- **Culture**: Use `CultureInfo.InvariantCulture` for all parsing and formatting.
- **Conditional compilation**: .NET 7+ uses source-generated regex via `[GeneratedRegex]` attributes on `private static partial Regex` methods. Older targets use `static readonly Regex` fields with `RegexOptions.Compiled`.
- **XML docs**: All public members have `<summary>`, `<param>`, `<returns>` documentation.
- **Tests**: xUnit with `[Theory]`/`[InlineData]` for parameterized tests. Test class naming: `{ClassName}Tests`.

## CI/CD

GitHub Actions workflow (`.github/workflows/nuget-publish.yml`) triggers on push to `release/*` branches or `v*` tags. It tests on .NET 8, 9, and 10, then packs and publishes to nuget.org from the .NET 10 matrix run using OIDC authentication.

## Branching

Release branches follow the pattern `release/{version}` (e.g., `release/3.2`). The current main development branch is `release/3.2`.
99 changes: 99 additions & 0 deletions OpenCode.Tests/DateTimeExtension/DateTimeExtension.UnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,103 @@ public void ToAge_ReturnsExpectedAge(string birthDateStr, int expectedAge)
var result = birthDate.ToAge();
Assert.Equal(expectedAge, result);
}

[Theory]
[InlineData(DayOfWeek.Monday, true)]
[InlineData(DayOfWeek.Friday, true)]
[InlineData(DayOfWeek.Saturday, false)]
[InlineData(DayOfWeek.Sunday, false)]
public void IsWeekday_ReturnsCorrectResult(DayOfWeek dayOfWeek, bool expected)
{
var date = new DateTime(2025, 10, 20).AddDays((int)dayOfWeek - (int)DayOfWeek.Monday);
Assert.Equal(expected, date.IsWeekday());
}

[Fact]
public void IsBetween_WithinRange_ReturnsTrue()
{
var date = new DateTime(2025, 6, 15);
var start = new DateTime(2025, 1, 1);
var end = new DateTime(2025, 12, 31);
Assert.True(date.IsBetween(start, end));
}

[Fact]
public void IsBetween_OutsideRange_ReturnsFalse()
{
var date = new DateTime(2026, 1, 1);
var start = new DateTime(2025, 1, 1);
var end = new DateTime(2025, 12, 31);
Assert.False(date.IsBetween(start, end));
}

[Fact]
public void IsBetween_OnBoundary_ReturnsTrue()
{
var start = new DateTime(2025, 1, 1);
Assert.True(start.IsBetween(start, new DateTime(2025, 12, 31)));
}

[Fact]
public void ToUnixTimestamp_ReturnsExpected()
{
var date = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var result = date.ToUnixTimestamp();
Assert.Equal(1735689600, result);
}

[Fact]
public void StartOfMonth_ReturnsFirstDay()
{
var date = new DateTime(2025, 10, 21, 15, 30, 45);
var result = date.StartOfMonth();
Assert.Equal(new DateTime(2025, 10, 1), result);
}

[Fact]
public void EndOfMonth_ReturnsLastDay()
{
var date = new DateTime(2025, 10, 15);
var result = date.EndOfMonth();
Assert.Equal(new DateTime(2025, 10, 31, 23, 59, 59, 999), result);
}

[Fact]
public void EndOfMonth_February_LeapYear()
{
var date = new DateTime(2024, 2, 10);
var result = date.EndOfMonth();
Assert.Equal(29, result.Day);
}

[Fact]
public void StartOfWeek_Monday_ReturnsMonday()
{
// 2025-10-23 is a Thursday
var date = new DateTime(2025, 10, 23);
var result = date.StartOfWeek(DayOfWeek.Monday);
Assert.Equal(new DateTime(2025, 10, 20), result);
Assert.Equal(DayOfWeek.Monday, result.DayOfWeek);
}

[Fact]
public void StartOfWeek_Sunday_ReturnsSunday()
{
// 2025-10-23 is a Thursday
var date = new DateTime(2025, 10, 23);
var result = date.StartOfWeek(DayOfWeek.Sunday);
Assert.Equal(new DateTime(2025, 10, 19), result);
Assert.Equal(DayOfWeek.Sunday, result.DayOfWeek);
}

[Theory]
[InlineData(2025, 2, 28)]
[InlineData(2024, 2, 29)]
[InlineData(2025, 1, 31)]
[InlineData(2025, 4, 30)]
public void DaysInMonth_ReturnsExpected(int year, int month, int expected)
{
var date = new DateTime(year, month, 1);
Assert.Equal(expected, date.DaysInMonth());
}
}
20 changes: 20 additions & 0 deletions OpenCode.Tests/DecimalExtension/DecimalExtension.UnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,24 @@ public void ToCurrency_ReturnsExpectedString(decimal value, string cultureName,
var result = value.ToCurrency(culture);
Assert.Equal(expected, result);
}

[Theory]
[InlineData(1.5, true)]
[InlineData(0.001, true)]
[InlineData(0, false)]
[InlineData(-1.5, false)]
public void IsPositive_ReturnsExpected(decimal value, bool expected)
{
Assert.Equal(expected, value.IsPositive());
}

[Theory]
[InlineData(-1.5, true)]
[InlineData(-0.001, true)]
[InlineData(0, false)]
[InlineData(1.5, false)]
public void IsNegative_ReturnsExpected(decimal value, bool expected)
{
Assert.Equal(expected, value.IsNegative());
}
}
31 changes: 31 additions & 0 deletions OpenCode.Tests/IntegerExtension/IntegerExtension.UnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,35 @@ public void Abs_ReturnsAbsoluteValue(int value, int expected)
{
Assert.Equal(expected, value.Abs());
}

[Theory]
[InlineData(5, 1, 10, true)]
[InlineData(1, 1, 10, true)]
[InlineData(10, 1, 10, true)]
[InlineData(0, 1, 10, false)]
[InlineData(11, 1, 10, false)]
public void IsBetween_ReturnsExpected(int value, int min, int max, bool expected)
{
Assert.Equal(expected, value.IsBetween(min, max));
}

[Theory]
[InlineData(1, true)]
[InlineData(100, true)]
[InlineData(0, false)]
[InlineData(-1, false)]
public void IsPositive_ReturnsExpected(int value, bool expected)
{
Assert.Equal(expected, value.IsPositive());
}

[Theory]
[InlineData(-1, true)]
[InlineData(-100, true)]
[InlineData(0, false)]
[InlineData(1, false)]
public void IsNegative_ReturnsExpected(int value, bool expected)
{
Assert.Equal(expected, value.IsNegative());
}
}
2 changes: 1 addition & 1 deletion OpenCode.Tests/OpenCode.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading