diff --git a/.agents/criteria-for-adding-quantities-and-units.md b/.agents/criteria-for-adding-quantities-and-units.md new file mode 100644 index 0000000000..a3ca300b90 --- /dev/null +++ b/.agents/criteria-for-adding-quantities-and-units.md @@ -0,0 +1,44 @@ +# Criteria for adding quantities and units + +See also: [Docs/adding-a-new-unit.md](../Docs/adding-a-new-unit.md#great-but-before-you-start) for the full contributor guide. + +To avoid bloating the library, we want to ensure quantities and units are widely used and well defined. +Avoid little used units that are obscure or too domain specific. +Ask for justification and use cases if this is not clear. + +### A quantity is a good fit to add, if it + +- [x] Is well documented and unambiguous, e.g. has a wiki page and generally easy to find on Google +- [x] Is widely used, preferably across domains +- [x] Has multiple units to convert between (e.g. `Length` has kilometer, feet, nanometer etc.) +- [x] Can convert to other quantities (e.g. `Length x Length = Area`) +- [x] Can be represented by a `double` numeric value, integer values are not well supported and may suffer from precision errors +- [x] Is not [dimensionless/unitless](https://en.wikipedia.org/wiki/Dimensionless_quantity) (consider using `Ratio`) + +Single-unit quantities are the exception. If a proposed quantity has only one unit, it needs stronger justification: the quantity must be widely used as a typed quantity representation on its own, not just as a unit abbreviation with no conversions. + +### A unit is a good fit to add to a quantity, if it + +- [x] Is well documented and unambiguous, e.g. has a wiki page or found in online unit converters +- [x] Is widely used +- [x] Can be converted to other units of the same quantity +- [x] The conversion function is well established without ambiguous competing standards + +### Review test values for new units + +- [x] Test values use literal constants, not expressions that repeat the JSON conversion functions +- [x] Test values preferably cite a verifiable source, such as an online converter, standard, or reference table +- [x] Test values have at least 7 significant figures where possible, but no more precision than `double` can usefully represent +- [x] The expected value independently sanity-checks the conversion function instead of mirroring its implementation + +### Review abbreviations for new units + +- [x] The primary abbreviation is the technically precise one when there is a clear distinction, such as `lbf`, `ozf`, or `gf` for force-derived compound units +- [x] Common shorthand variants such as `lb`, `oz`, or `g` are only secondary abbreviations, and only when they are widely used and unambiguous for that quantity +- [x] Shorthand aliases do not create ambiguity with another unit in the same quantity +- [x] Common shorthand aliases that fit multiple quantities are mapped to each relevant quantity, e.g. `ft-lb` for torque and energy, or `oz·in` for torque and static unbalance +- [x] Singular unit symbols such as `lb` and `oz` are preferred over pluralized forms such as `lbs` and `ozs`; pluralized forms should only be secondary aliases when they are strong domain conventions + +### Avoid X-per-Y units + +There are many variations of unit A over unit B, such as `LengthPerAngle` and we want to avoid adding these unless they are very common. diff --git a/.claude/criteria-for-adding-quantities-and-units.md b/.claude/criteria-for-adding-quantities-and-units.md deleted file mode 100644 index ab0766c477..0000000000 --- a/.claude/criteria-for-adding-quantities-and-units.md +++ /dev/null @@ -1,27 +0,0 @@ -# Criteria for adding quantities and units - -See also: [Docs/adding-a-new-unit.md](../Docs/adding-a-new-unit.md#great-but-before-you-start) for the full contributor guide. - -To avoid bloating the library, we want to ensure quantities and units are widely used and well defined. -Avoid little used units that are obscure or too domain specific. -Ask for justification and use cases if this is not clear. - -### A quantity is a good fit to add, if it - -- [x] Is well documented and unambiguous, e.g. has a wiki page and generally easy to find on Google -- [x] Is widely used, preferably across domains -- [x] Has multiple units to convert between (e.g. `Length` has kilometer, feet, nanometer etc.) -- [x] Can convert to other quantities (e.g. `Length x Length = Area`) -- [x] Can be represented by a `double` numeric value, integer values are not well supported and may suffer from precision errors -- [x] Is not [dimensionless/unitless](https://en.wikipedia.org/wiki/Dimensionless_quantity) (consider using `Ratio`) - -### A unit is a good fit to add to a quantity, if it - -- [x] Is well documented and unambiguous, e.g. has a wiki page or found in online unit converters -- [x] Is widely used -- [x] Can be converted to other units of the same quantity -- [x] The conversion function is well established without ambiguous competing standards - -### Avoid X-per-Y units - -There are many variations of unit A over unit B, such as `LengthPerAngle` and we want to avoid adding these unless they are very common. \ No newline at end of file diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e9d3dfb6dc..b328674053 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,11 +2,12 @@ "version": 1, "isRoot": true, "tools": { - "jetbrains.dotcover.globaltool": { - "version": "2022.2.3", + "jetbrains.dotcover.commandlinetools": { + "version": "2025.1.8", "commands": [ - "dotnet-dotcover" - ] + "dotCover" + ], + "rollForward": false }, "timeitsharp": { "version": "0.0.8", @@ -15,4 +16,4 @@ ] } } -} \ No newline at end of file +} diff --git a/.devcontainer/unitsnet-modular/devcontainer.json b/.devcontainer/unitsnet-modular/devcontainer.json new file mode 100644 index 0000000000..9c8c226bd2 --- /dev/null +++ b/.devcontainer/unitsnet-modular/devcontainer.json @@ -0,0 +1,22 @@ +{ + "name": "UnitsNet.Modular samples", + "image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble", + "postCreateCommand": "dotnet build UnitsNet.Modular/Samples/UnitsNet.Modular.Samples.slnx -p:Platform=ProjectReferences", + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit" + ], + "settings": { + "dotnet.defaultSolution": "UnitsNet.Modular/Samples/UnitsNet.Modular.Samples.slnx", + "csharp.debug.console": "integratedTerminal", + "terminal.integrated.cwd": "${workspaceFolder}/UnitsNet.Modular/Samples" + } + }, + "codespaces": { + "openFiles": [ + "UnitsNet.Modular/Samples/README.md" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig index 1a45a47681..0e7f9de86f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,13 +8,13 @@ root = true [*] indent_style = space trim_trailing_whitespace = true +charset = utf-8 # (Please don't specify an indent_size here; that has too many unintended consequences.) # Code files [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true -charset = utf-8-bom [*.{cmd,bat}] indent_size = 2 @@ -25,7 +25,6 @@ insert_final_newline = false indent_size = 2 end_of_line = crlf insert_final_newline = true -charset = utf-8-bom # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] diff --git a/.github/actions/run-benchmarks/action.yaml b/.github/actions/run-benchmarks/action.yaml index c865add65b..f420fa7ac2 100644 --- a/.github/actions/run-benchmarks/action.yaml +++ b/.github/actions/run-benchmarks/action.yaml @@ -1,13 +1,13 @@ name: 'Run benchmark' inputs: framework: - description: 'The runtime version to use (e.g. net5.0)' + description: 'The target framework used to host the benchmarks (e.g. net10.0)' required: false - default: 'net5.0' + default: 'net10.0' runtimes: - description: 'The runtime version to use (e.g. netcoreapp31, net5.0)' + description: 'The runtime versions to benchmark (e.g. net10.0, net9.0, net48)' required: false - default: 'net5.0' + default: 'net10.0' output-folder: description: 'The output folder for the benchmark (a results folder is created inside)' required: false @@ -40,4 +40,4 @@ runs: --exporters ${{ inputs.exporters }} --filter '${{ inputs.filter }}' ${{ inputs.categories }} ${{ inputs.execution-options }} - shell: bash \ No newline at end of file + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5feb563ec4..1e00d266c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,43 @@ on: - 'release/**' - 'maintenance/**' paths-ignore: - - '**/*.png' - '**/*.md' + - '**/*.png' workflow_dispatch: + inputs: + run_tests: + description: Run tests + required: true + default: true + type: boolean + collect_coverage: + description: Collect and upload coverage (requires tests) + required: true + default: true + type: boolean + pack_nugets: + description: Pack and upload NuGet artifacts + required: true + default: true + type: boolean + upload_artifacts: + description: Create and upload the full artifact archive + required: true + default: true + type: boolean + publish_nuget: + description: Publish to nuget.org (master only; forces packing) + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + # Never interrupt a master run while it may be publishing an immutable package set. + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true @@ -17,93 +51,110 @@ env: jobs: build-and-test: - name: Build & Test - runs-on: windows-latest + name: CI Build & Test + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 1 lfs: true - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v6 with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x + 10.0.x - - name: Setup .NET nanoFramework build components - uses: nanoframework/nanobuild@v1 - with: - workload: 'nanoFramework' - - - name: Build, Test and Pack - shell: pwsh - run: | - ./Build/build.ps1 -IncludeNanoFramework - working-directory: ${{ github.workspace }} - - - name: Upload to codecov.io + - name: Build selected components shell: pwsh env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + RUN_TESTS: ${{ github.event_name != 'workflow_dispatch' || inputs.run_tests }} + COLLECT_COVERAGE: ${{ github.event_name != 'workflow_dispatch' || inputs.collect_coverage }} + PACK_NUGETS: ${{ github.event_name != 'workflow_dispatch' || inputs.pack_nugets || inputs.publish_nuget }} + CREATE_ARCHIVE: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_artifacts }} run: | - Write-Host -Foreground Green "Downloading codecov binaries..." - - Invoke-WebRequest -Uri https://uploader.codecov.io/verification.gpg -OutFile codecov.asc - gpg.exe --import codecov.asc - - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe.SHA256SUM -Outfile codecov.exe.SHA256SUM - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe.SHA256SUM.sig -Outfile codecov.exe.SHA256SUM.sig - - gpg.exe --verify codecov.exe.SHA256SUM.sig codecov.exe.SHA256SUM - If ($(Compare-Object -ReferenceObject $(($(certUtil -hashfile codecov.exe SHA256)[1], "codecov.exe") -join " ") -DifferenceObject $(Get-Content codecov.exe.SHA256SUM)).length -eq 0) { echo "SHASUM verified" } Else {exit 1} - - Write-Host -Foreground Green "Uploading to codecov..." - - .\codecov.exe --dir "Artifacts/Coverage" -t "$env:CODECOV_TOKEN" --build "${{ github.run_number }}" - - Write-Host -Foreground Green "✅ Uploaded to codecov." - - - name: Upload Artifacts - uses: actions/upload-artifact@v4 + $buildArguments = @() + if ($env:RUN_TESTS -ne 'true') { $buildArguments += '-SkipTests' } + if ($env:COLLECT_COVERAGE -ne 'true') { $buildArguments += '-SkipCoverage' } + if ($env:PACK_NUGETS -ne 'true') { $buildArguments += '-SkipPack' } + if ($env:CREATE_ARCHIVE -ne 'true') { $buildArguments += '-SkipArchive' } + ./Build/build.ps1 @buildArguments + + # Codecov intermittently fails while importing its verification key from Keybase. + # Keep coverage non-blocking until upstream adds reliable retry/error handling: + # https://github.com/codecov/codecov-action/issues/1876 + # https://github.com/codecov/wrapper/pull/66 + - name: Upload coverage to Codecov + if: github.event_name != 'workflow_dispatch' || (inputs.run_tests && inputs.collect_coverage) + continue-on-error: true + uses: codecov/codecov-action@v6 + with: + directory: Artifacts/Coverage + fail_ci_if_error: true + plugins: noop + use_oidc: true + + - name: Upload artifacts + if: github.event_name != 'workflow_dispatch' || inputs.upload_artifacts + uses: actions/upload-artifact@v7 with: name: artifacts path: Artifacts/ retention-days: 30 - name: Upload NuGet packages - uses: actions/upload-artifact@v4 + if: github.event_name != 'workflow_dispatch' || inputs.pack_nugets || inputs.publish_nuget + uses: actions/upload-artifact@v7 with: name: nuget-packages path: | Artifacts/**/*.nupkg Artifacts/**/*.snupkg + if-no-files-found: error retention-days: 30 publish-nuget: - name: Publish to NuGet + name: Publish NuGet packages needs: build-and-test runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' && github.repository_owner == 'angularsen' + timeout-minutes: 10 + if: >- + github.ref == 'refs/heads/master' && + github.repository_owner == 'angularsen' && + (github.event_name == 'push' || inputs.publish_nuget) environment: Publish + permissions: + contents: read + id-token: write steps: - name: Download NuGet packages - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: nuget-packages path: nugets - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Log in to NuGet with trusted publishing + uses: NuGet/login@v1 + id: nuget-login with: - dotnet-version: 8.0.x + user: angularsen - name: Push to nuget.org - run: | - dotnet nuget push "**/*.nupkg" --skip-duplicate --api-key ${{ secrets.NUGET_ORG_APIKEY }} --source https://api.nuget.org/v3/index.json - working-directory: nugets \ No newline at end of file + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + run: dotnet nuget push "**/*.nupkg" --skip-duplicate --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json + working-directory: nugets diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 6914ea916a..7097648876 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,6 +12,9 @@ on: jobs: claude-review: + # Fork PRs cannot request OIDC tokens and contain untrusted input. A + # maintainer can request a review explicitly by commenting `@claude`. + if: github.event.pull_request.head.repo.full_name == github.repository # Optional: Filter by PR author # if: | # github.event.pull_request.user.login == 'external-contributor' || @@ -41,7 +44,7 @@ jobs: - Breaking changes, if any - Style and conventions - New quantities or units - - See `.claude/pr-review-instructions.md` for guidance and criteria, they should be widely used and well defined + - See `.agents/criteria-for-adding-quantities-and-units.md` for guidance and criteria, they should be widely used and well defined - If it seems domain specific or obscure we ask for justification and use cases - Changes to generated code - Focus feedback on changes to code generators @@ -55,6 +58,22 @@ jobs: Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback, keep it concise. + Make the review outcome and every bullet point easy to scan: + - Start with exactly one of these headings: + - `## ✅ Review: No actionable findings` + - `## ⚠️ Review: Non-blocking findings` + - `## ❌ Review: Blocking findings` + - Prefix every section heading with an emoji that summarizes its most severe bullet, + for example `### ✅ Test coverage`, `### ⚠️ Breaking changes`, or `### ❌ Code quality & correctness`. + - For both the overall review and each section, use ❌ if it contains any defects or blocking issues, + otherwise ⚠️ if it contains any non-blocking concerns, questions, or suggestions, otherwise ✅. + - Prefix every feedback bullet with exactly one status emoji: + - ✅ for a positive verification or something that is correct as-is + - ⚠️ for a non-blocking concern, question, or suggestion + - ❌ for a defect or blocking issue that should be fixed + - Keep each bullet to one status. If a point contains both positive and negative feedback, split it into separate bullets. + - When a section has no findings, either omit it or write `✅ No concerns identified.` + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 95c7ece36b..4fb53e874f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -40,10 +40,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - # .NET 6 is used in CodeGen project. - - uses: actions/setup-dotnet@v1 + # CodeGen and the standard build/test toolchain use .NET 10. + - uses: actions/setup-dotnet@v6 with: - dotnet-version: '6.0.x' + dotnet-version: '10.0.x' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/continious-benchmarking.yml b/.github/workflows/continious-benchmarking.yml index 24b5b7421e..9b4dd65e1c 100644 --- a/.github/workflows/continious-benchmarking.yml +++ b/.github/workflows/continious-benchmarking.yml @@ -9,7 +9,7 @@ on: - ".github/actions/**" env: - FRAMEWORK: net6.0 + FRAMEWORK: net10.0 EXECUTION_OPTIONS: --iterationTime 500 --disableLogFile # see https://benchmarkdotnet.org/articles/guides/console-args.html BENCHMARK_PAGES_BRANCH: gh-pages BENCHMARK_DATA_FOLDER: benchmarks @@ -21,25 +21,19 @@ jobs: strategy: # max-parallel: 1 # is it better to avoid running in parallel? matrix: - runtime: ["net6.0", "netcoreapp21", "net472"] + runtime: ["net10.0", "net9.0", "net48"] steps: - run: echo Starting benchmarks for ${{ matrix.runtime }} # checkout the current branch - uses: actions/checkout@v2 - # we need all frameworks (even if only running one target at a time) - - uses: actions/setup-dotnet@v1 + # The benchmark host targets net10, with net9 installed for runtime comparisons. + - uses: actions/setup-dotnet@v6 with: - dotnet-version: "2.1.x" - - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "3.1.x" - - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "5.0.x" + dotnet-version: | + 9.0.x + 10.0.x # executing the benchmark for the current framework, placing the result in a corresponding sub-folder - uses: ./.github/actions/run-benchmarks @@ -62,7 +56,7 @@ jobs: strategy: max-parallel: 1 # cannot commit on the same branch in parallel matrix: - runtime: ["net6.0", "netcoreapp21", "net472"] + runtime: ["net10.0", "net9.0", "net48"] steps: - name: Initializing git folder ️ uses: actions/checkout@v2.3.1 diff --git a/.github/workflows/net48-compatibility.yml b/.github/workflows/net48-compatibility.yml new file mode 100644 index 0000000000..fc1edadbae --- /dev/null +++ b/.github/workflows/net48-compatibility.yml @@ -0,0 +1,146 @@ +name: net48 Compatibility + +on: + # Remove `pull_request` for post-merge validation only, or remove `push` for PR validation only. + # Test and coverage behavior is identical for every enabled trigger. + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: net48-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + test: + name: ${{ github.event_name == 'pull_request' && 'PR net48 Build & Test' || 'CI net48 Build & Test' }} + runs-on: windows-latest + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 1 + lfs: true + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Generate code + run: dotnet run --project CodeGen + + - name: Run the complete net48 test suite with coverage + shell: pwsh + run: | + Import-Module ./Build/test-projects.psm1 -Force + $testProjectPaths = @(Get-TestProjectPaths) + + New-Item -ItemType Directory -Force -Path Artifacts/Coverage | Out-Null + + foreach ($testProject in $testProjectPaths) { + $projectName = [IO.Path]::GetFileNameWithoutExtension($testProject) + + # Keep compiler servers outside dotCover's process tree. Roslyn's VBCSCompiler has a + # 10-minute idle timeout, which can otherwise delay coverage-session finalization. + Write-Host "Building $projectName at $([DateTime]::UtcNow.ToString('O'))" + dotnet build $testProject ` + --configuration Release ` + --framework net48 ` + -p:ContinuousIntegrationBuild=true + + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + $testArguments = @( + $testProject, + '--configuration', 'Release', + '--framework', 'net48', + '--no-build', + '--no-restore', + '--logger', 'trx', + '--results-directory', 'Artifacts/TestResults' + ) + + Write-Host "Testing $projectName with coverage at $([DateTime]::UtcNow.ToString('O'))" + dotnet tool run dotCover -- cover-dotnet ` + --TargetWorkingDir $PWD.Path ` + --Output "Artifacts/Coverage/$projectName.coverage.xml" ` + --ReportType DetailedXML ` + --Filters '+:module=UnitsNet*;-:module=*Tests' ` + --ProcessFilters '-:VBCSCompiler*' ` + --LogFile "Artifacts/Coverage/$projectName.dotcover.log" ` + --ReturnTargetExitCode ` + -- test @testArguments + + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + } + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v7 + with: + name: net48-test-results + path: Artifacts/TestResults/*.trx + if-no-files-found: ignore + retention-days: 30 + + # publish-net48-test-results.yml defines the "Publish net48 Test Results" + # workflow_run workflow that publishes these results with checks: write. Keeping that permission + # out of this workflow lets fork PRs execute contributor code with a read-only token. + - name: Upload workflow event + if: always() + uses: actions/upload-artifact@v7 + with: + name: net48-event + path: ${{ github.event_path }} + retention-days: 30 + + - name: Upload coverage to Codecov + if: ${{ success() && hashFiles('Artifacts/Coverage/*.coverage.xml') != '' }} + uses: codecov/codecov-action@v6 + with: + directory: Artifacts/Coverage + fail_ci_if_error: ${{ github.event_name != 'pull_request' }} + flags: net48 + name: net48-clr4 + plugins: noop + use_oidc: true + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: net48-coverage + path: Artifacts/Coverage/*.coverage.xml + if-no-files-found: ignore + retention-days: 30 + + - name: Upload dotCover logs + if: ${{ failure() || cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: net48-dotcover-logs + path: Artifacts/Coverage/*.dotcover.log + if-no-files-found: ignore + retention-days: 30 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b619c7ae0f..e02d98af61 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -7,9 +7,13 @@ on: - 'release/**' - 'maintenance/**' paths-ignore: - - '*.md' - - '*.png' - - '*.gitignore' + - '**/*.gitignore' + - '**/*.md' + - '**/*.png' + +concurrency: + group: pr-${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true @@ -17,77 +21,61 @@ env: jobs: build-and-test: - name: Build & Test - runs-on: windows-latest + name: PR Build & Test + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + checks: write + id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 1 lfs: true - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v6 with: dotnet-version: | - 6.0.x 8.0.x + 9.0.x + 10.0.x - - name: Setup .NET nanoFramework build components - uses: nanoframework/nanobuild@v1 - with: - workload: 'nanoFramework' - - - name: Build, Test and Pack + - name: Build, test and pack shell: pwsh - run: | - ./Build/build.ps1 -IncludeNanoFramework - working-directory: ${{ github.workspace }} + run: ./Build/build.ps1 - - name: Upload Test Results - uses: actions/upload-artifact@v4 + - name: Upload test results if: always() + uses: actions/upload-artifact@v7 with: name: test-results path: Artifacts/TestResults/*.trx + if-no-files-found: ignore retention-days: 7 - - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action/windows@v2 - if: always() + - name: Publish test results + if: ${{ !cancelled() && github.event.pull_request.head.repo.full_name == github.repository && hashFiles('Artifacts/TestResults/*.trx') != '' }} + uses: EnricoMi/publish-unit-test-result-action@v2 with: - files: | - Artifacts/TestResults/*.trx - check_name: Test Results + files: Artifacts/TestResults/*.trx + check_name: PR Test Results comment_mode: off - - name: Upload to codecov.io - shell: pwsh - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - run: | - Write-Host -Foreground Green "Downloading codecov binaries..." - - Invoke-WebRequest -Uri https://uploader.codecov.io/verification.gpg -OutFile codecov.asc - gpg.exe --import codecov.asc - - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe.SHA256SUM -Outfile codecov.exe.SHA256SUM - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe.SHA256SUM.sig -Outfile codecov.exe.SHA256SUM.sig - - gpg.exe --verify codecov.exe.SHA256SUM.sig codecov.exe.SHA256SUM - If ($(Compare-Object -ReferenceObject $(($(certUtil -hashfile codecov.exe SHA256)[1], "codecov.exe") -join " ") -DifferenceObject $(Get-Content codecov.exe.SHA256SUM)).length -eq 0) { echo "SHASUM verified" } Else {exit 1} - - Write-Host -Foreground Green "Uploading to codecov..." - - .\codecov.exe --dir "Artifacts/Coverage" -t "$env:CODECOV_TOKEN" --build "${{ github.run_number }}" - - Write-Host -Foreground Green "✅ Uploaded to codecov." + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v6 + with: + directory: Artifacts/Coverage + fail_ci_if_error: false + plugins: noop + use_oidc: true - - name: Upload Artifacts - uses: actions/upload-artifact@v4 + - name: Upload artifacts + uses: actions/upload-artifact@v7 with: name: artifacts path: Artifacts/ - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/.github/workflows/publish-net48-test-results.yml b/.github/workflows/publish-net48-test-results.yml new file mode 100644 index 0000000000..29f03fa507 --- /dev/null +++ b/.github/workflows/publish-net48-test-results.yml @@ -0,0 +1,50 @@ +name: Publish net48 Test Results + +on: + workflow_run: + workflows: + - net48 Compatibility + types: + - completed + +# This workflow receives write permission because it never checks out or executes contributor +# code. It only downloads immutable artifacts from the exact workflow run that triggered it. +permissions: {} + +jobs: + publish: + name: ${{ github.event.workflow_run.event == 'pull_request' && 'Publish PR net48 Test Results' || 'Publish CI net48 Test Results' }} + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + actions: read + checks: write + + steps: + - name: Download test results + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: net48-test-results + path: artifacts/net48-test-results + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Download workflow event + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: net48-event + path: artifacts/net48-event + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Publish test results + if: ${{ hashFiles('artifacts/net48-test-results/*.trx') != '' }} + uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2 + with: + check_name: ${{ github.event.workflow_run.event == 'pull_request' && 'PR net48 Test Results' || 'CI net48 Test Results' }} + comment_mode: off + commit: ${{ github.event.workflow_run.head_sha }} + event_file: artifacts/net48-event/event.json + event_name: ${{ github.event.workflow_run.event }} + files: artifacts/net48-test-results/*.trx diff --git a/.github/workflows/run-benchmarks.yml b/.github/workflows/run-benchmarks.yml index 344d765b6b..2ee4e66997 100644 --- a/.github/workflows/run-benchmarks.yml +++ b/.github/workflows/run-benchmarks.yml @@ -7,8 +7,8 @@ on: required: false default: "UnitsNet Benchmarks" runtimes: - description: "The runtime version to use (e.g. net472 net48 netcoreapp21 netcoreapp31 net6.0)" - default: net472 netcoreapp21 net6.0 + description: "The runtime versions to use (e.g. net10.0 net9.0 net48)" + default: net10.0 net9.0 net48 required: true exporters: description: "The exporter(s) used for this run (GitHub/StackOverflow/RPlot/CSV/JSON/HTML/XML)" @@ -29,7 +29,7 @@ on: comparison-baseline: description: "Compare against a previous result (expecting a link to *-report-full.json)" required: true - default: "https://angularsen.github.io/UnitsNet/benchmarks/netcoreapp50/results/UnitsNet.Benchmark.UnitsNetBenchmarks-report-full.json" + default: "https://angularsen.github.io/UnitsNet/benchmarks/net10.0/results/UnitsNet.Benchmark.UnitsNetBenchmarks-report-full.json" comparison-threshold: description: "The (comparison) threshold for Statistical Test. Examples: 5%, 10ms, 100ns, 1s" required: false @@ -39,8 +39,8 @@ on: required: false default: 10 framework: - description: "The dotnet-version version to use (e.g. net6.0)" - default: "net6.0" + description: "The target framework used to host the benchmarks" + default: "net10.0" required: true jobs: benchmark: @@ -51,18 +51,12 @@ jobs: # checkout the current branch - uses: actions/checkout@v2 - # we need all frameworks (even if only running one target at a time) - - uses: actions/setup-dotnet@v1 + # The benchmark host targets net10, with net9 installed for runtime comparisons. + - uses: actions/setup-dotnet@v6 with: - dotnet-version: "2.1.x" - - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "3.1.x" - - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "5.0.x" + dotnet-version: | + 9.0.x + 10.0.x # executing the benchmark for the current framework(s), placing the result in the output-folder - uses: ./.github/actions/run-benchmarks @@ -110,19 +104,19 @@ jobs: repository: dotnet/performance path: comparer - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v6 with: - dotnet-version: "3.1.x" + dotnet-version: "10.0.x" - run: mkdir -p artifacts # Executing the comparer, placing the result in a 'comparison' folder (as well as creating a summary from the output) - name: Running the ResultsComparer env: - PERFLAB_TARGET_FRAMEWORKS: net6.0 + PERFLAB_TARGET_FRAMEWORKS: net10.0 run: > dotnet run --project 'comparer/src/tools/ResultsComparer' -c Release - --framework net6.0 + --framework net10.0 --base baseline --diff results --csv "artifacts/${{ env.OUPUT_NAME }}.csv" --xml "artifacts/${{ env.OUPUT_NAME }}.xml" diff --git a/.github/workflows/unitsnet-modular-ci.yml b/.github/workflows/unitsnet-modular-ci.yml new file mode 100644 index 0000000000..6b262610e0 --- /dev/null +++ b/.github/workflows/unitsnet-modular-ci.yml @@ -0,0 +1,186 @@ +name: UnitsNet.Modular CI + +on: + push: + branches: + - master + tags: + - 'UnitsNet.Modular/*' + paths: + - '.devcontainer/unitsnet-modular/**' + - 'UnitsNet.Modular/**' + # The compatibility suite compiles the current UnitsNet project to detect source/API drift. + - 'UnitsNet/**' + - 'Common/UnitDefinitions/**' + - 'Common/UnitRelations.json' + - 'Common/UnitEnumValues.g.json' + - 'UnitsNet.Modular.slnx' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'NuGet.Config' + - 'NullableAttributes.cs' + - 'global.json' + - '.github/workflows/unitsnet-modular-ci.yml' + pull_request: + branches: + - master + paths: + - '.devcontainer/unitsnet-modular/**' + - 'UnitsNet.Modular/**' + # The compatibility suite compiles the current UnitsNet project to detect source/API drift. + - 'UnitsNet/**' + - 'Common/UnitDefinitions/**' + - 'Common/UnitRelations.json' + - 'Common/UnitEnumValues.g.json' + - 'UnitsNet.Modular.slnx' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'NuGet.Config' + - 'NullableAttributes.cs' + - 'global.json' + - '.github/workflows/unitsnet-modular-ci.yml' + workflow_dispatch: + inputs: + publish_nuget: + description: Publish to nuget.org (UnitsNet.Modular/* tag only) + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: unitsnet-modular-ci-${{ github.ref }} + # Never interrupt a release-tag run while it may be publishing immutable packages. + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/UnitsNet.Modular/') }} + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + build-test-pack: + name: Build, test, and pack + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout full history + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore UnitsNet.Modular.slnx -p:Platform=ProjectReferences + + - name: Build + run: dotnet build UnitsNet.Modular.slnx --configuration Release --no-restore --no-incremental -p:Platform=ProjectReferences + + - name: Test + run: dotnet test UnitsNet.Modular.slnx --configuration Release --no-build -p:Platform=ProjectReferences + + - name: Native AOT smoke test + run: | + dotnet publish UnitsNet.Modular/Samples/QuantitySelectionSample/QuantitySelectionSample.csproj \ + --configuration Release \ + --framework net10.0 \ + --runtime linux-x64 \ + --self-contained true \ + -p:PublishAot=true \ + -p:Platform=ProjectReferences \ + --output Artifacts/UnitsNet.Modular.AotSmoke + ./Artifacts/UnitsNet.Modular.AotSmoke/QuantitySelectionSample + + - name: Run getting-started NuGet consumer + shell: pwsh + run: ./UnitsNet.Modular/Samples/GettingStartedSample/run.ps1 + + - name: Run custom-definition NuGet consumer with isolated cache + run: >- + dotnet run + --project UnitsNet.Modular/Samples/CustomQuantitySample/CustomQuantitySample.csproj + --configuration Debug + -p:Platform=LocalPackages + -p:RestorePackagesPath=${{ runner.temp }}/unitsnet-modular-custom-packages-${{ github.run_id }}-${{ github.run_attempt }} + + - name: Run published-package consumer + run: >- + dotnet run + --project UnitsNet.Modular/Samples/GettingStartedSample/GettingStartedSample.csproj + --configuration Release + -p:Platform=PublishedPackages + + - name: Pack + run: dotnet pack UnitsNet.Modular/UnitsNet.Modular/UnitsNet.Modular.csproj --configuration Release --no-build --output Artifacts/UnitsNet.Modular.CI -p:UnitsNetModularPackForPublish=true + + - name: Verify tagged package versions + if: startsWith(github.ref, 'refs/tags/UnitsNet.Modular/') + shell: pwsh + run: | + $tagRefPrefix = 'refs/tags/UnitsNet.Modular/' + if (-not $env:GITHUB_REF.StartsWith($tagRefPrefix, [StringComparison]::Ordinal)) { + throw "Expected ref '$env:GITHUB_REF' to start with '$tagRefPrefix'." + } + + $expectedVersion = $env:GITHUB_REF.Substring($tagRefPrefix.Length) + $packageDirectory = 'Artifacts/UnitsNet.Modular.CI' + $packagePath = Join-Path $packageDirectory "UnitsNet.Modular.$expectedVersion.nupkg" + if (-not (Test-Path -LiteralPath $packagePath -PathType Leaf)) { + throw "Expected tagged package was not produced: $packagePath" + } + + - name: Upload packages + uses: actions/upload-artifact@v7 + with: + name: unitsnet-modular-packages-${{ github.sha }} + path: | + Artifacts/UnitsNet.Modular.CI/*.nupkg + Artifacts/UnitsNet.Modular.CI/*.snupkg + if-no-files-found: error + retention-days: 14 + + publish-nuget: + name: Publish NuGet packages + needs: build-test-pack + runs-on: ubuntu-latest + timeout-minutes: 10 + if: >- + github.repository_owner == 'angularsen' && + startsWith(github.ref, 'refs/tags/UnitsNet.Modular/') && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs.publish_nuget)) + environment: Publish + permissions: + contents: read + id-token: write + + steps: + - name: Download NuGet packages + uses: actions/download-artifact@v8 + with: + name: unitsnet-modular-packages-${{ github.sha }} + path: nugets + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Log in to NuGet with trusted publishing + uses: NuGet/login@v1 + id: nuget-login + with: + user: angularsen + + - name: Push to nuget.org + env: + NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} + run: dotnet nuget push "**/*.nupkg" --skip-duplicate --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json + working-directory: nugets diff --git a/.gitignore b/.gitignore index 9ba97caeb1..2363aca503 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.userosscache *.sln.docstates +.temp/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -23,7 +24,8 @@ bld/ [Ll]og/ # VS Code dir -.vscode/ +.vscode/* +!.vscode/launch.json # Visual Studio 2015 cache/options directory .vs/ @@ -262,3 +264,12 @@ Artifacts # Claude Code assistant .claude/settings.local.json +.claude/worktrees/ +.DS_Store + +# Keep the shared repository-local NuGet feed present on clean checkouts. +!Artifacts/ +Artifacts/* +!Artifacts/Nugets/ +Artifacts/Nugets/* +!Artifacts/Nugets/.gitkeep diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..4047d68dd6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,41 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Sample: Getting started", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/UnitsNet.Modular/Samples/GettingStartedSample/GettingStartedSample.csproj" + }, + { + "name": "Sample: Quantity selection", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/UnitsNet.Modular/Samples/QuantitySelectionSample/QuantitySelectionSample.csproj" + }, + { + "name": "Sample: Custom quantity", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/UnitsNet.Modular/Samples/CustomQuantitySample/CustomQuantitySample.csproj" + }, + { + "name": "Sample: All SI profile", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/UnitsNet.Modular/Samples/Profiles/AllSiProfileSample/AllSiProfileSample.csproj" + }, + { + "name": "Sample: Modular playground", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/UnitsNet.Modular/Samples/ModularPlayground/ModularPlayground.csproj" + }, + { + "name": "Sample: Shared units library", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/UnitsNet.Modular/Samples/SharedUnitsLibrarySample/SharedUnitsLibrarySample.App/SharedUnitsLibrarySample.App.csproj" + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..28ff7390e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md + +This file provides shared guidance for AI coding agents when working with code in this repository. + +## Project Overview + +UnitsNet is a .NET library that provides strongly-typed physical units and quantities, enabling safe and intuitive unit conversions in code. The library uses code generation from JSON definitions to create type-safe APIs for over 130 physical quantities. + +## Key Commands + +### Build and Test +- **Build project**: `build.bat` or `dotnet build UnitsNet.slnx` +- **Run tests**: `test.bat` or `dotnet test UnitsNet.slnx` +- **Run single test**: `dotnet test UnitsNet.Tests --filter "FullyQualifiedName~TestClassName.TestMethodName"` +- **Clean artifacts**: `clean.bat` + +### Code Generation +- **Generate code from JSON definitions**: `generate-code.bat` or `dotnet run --project CodeGen` + - Always run this after modifying any JSON files in `Common/UnitDefinitions/` + - The generator reads 131 JSON definition files and creates C# code + +### Development Workflow +1. Modify unit definitions in `Common/UnitDefinitions/*.json` +2. Run `generate-code.bat` to regenerate C# code +3. Run `build.bat` to compile and test +4. Use `test.bat` for isolated test runs + +## Code Architecture + +### Project Structure +- **UnitsNet/**: Main library with quantity types and units + - `GeneratedCode/`: Auto-generated from JSON definitions (do not edit manually) + - `CustomCode/`: Hand-written code extending generated types +- **UnitsNet.Tests/**: Comprehensive test suite +- **CodeGen/**: Code generation tool that creates C# from JSON definitions +- **Common/UnitDefinitions/**: 131 JSON files defining physical quantities +- **UnitsNet.NumberExtensions/**: Extension methods for numeric types +- **UnitsNet.Serialization.*/**: JSON.NET and System.Text.Json serialization support + +### Code Generation Process +The project uses a sophisticated code generation system: +1. JSON definitions in `Common/UnitDefinitions/` describe units, conversions, and localizations +2. `CodeGen` project processes these to generate: + - Quantity types (e.g., `Length`, `Mass`) + - Unit enums (e.g., `LengthUnit`, `MassUnit`) + - Conversion logic and unit abbreviations +3. Generated code goes to `*/GeneratedCode/` folders +4. Custom code in `*/CustomCode/` extends generated types + +### Key Classes and Patterns +- **IQuantity**: Base interface for all quantity types +- **Quantity**: Static class for dynamic quantity operations +- **UnitConverter**: Handles conversions between units +- **QuantityParser/UnitParser**: Parse strings to quantities/units +- **UnitsNetSetup**: Configuration singleton + +### Adding or Modifying Units +1. Edit or create JSON file in `Common/UnitDefinitions/` +2. Follow conversion function guidelines in [Docs/adding-a-new-unit.md](Docs/adding-a-new-unit.md): + - Use multiplication for `FromUnitToBaseFunc` + - Use division for `FromBaseToUnitFunc` + - Prefer scientific notation (1e3, 1e-5) + - Use exact constituent constants instead of pre-computed decimals +3. Run `generate-code.bat` +4. Add tests if needed + +## Important Conventions + +### Coding Standards +- Follow `.editorconfig` specifications +- Use ReSharper settings in `UnitsNet.sln.DotSettings` +- Treat warnings as errors (except obsolete warnings) +- Add file headers to new files + +### Unit Definition Rules +- Base units are chosen for each quantity (e.g., meter for Length) +- All conversions go through the base unit +- Use superscript in abbreviations: cm², m³ +- Compound units format: N·m (dot), km/h (slash) + +### Testing +- Test class naming: `Tests` +- Test method naming: `__` +- Tests accept error margin of 1E-5 for most units + +## Special Considerations + +### Performance +- Conversion functions are compiled to delegates for performance +- All conversions go through base units (potential for small errors) +- Precision goal is 1E-5 for most units + +### Localization +- Unit abbreviations support multiple cultures +- JSON definitions include translations for various languages +- Default culture: Thread.CurrentCulture, fallback to en-US + +## Common Tasks + +### Find specific quantity or unit implementation +- Quantity types: `UnitsNet/GeneratedCode/Quantities/*.g.cs` +- Unit enums: `UnitsNet/GeneratedCode/Units/*.g.cs` +- Custom extensions: `UnitsNet/CustomCode/Quantities/*.extra.cs` +- Unit definitions: `Common/UnitDefinitions/*.json` + +### Debug code generation +- Generator entry: `CodeGen/Program.cs` +- Generator logic: `CodeGen/Generators/` +- Enable verbose logging: Check Serilog configuration in Program.cs + +### Run performance benchmarks +- Execute: `dotnet run -c Release --project UnitsNet.Benchmark` +- Results saved to `Artifacts/` folder + +## Documentation + +All contributor and user documentation lives in [Docs/](Docs/README.md), including: +- [Adding a New Unit](Docs/adding-a-new-unit.md) - step-by-step guide with JSON schema conventions +- [Adding Operator Overloads](Docs/adding-operator-overloads.md) +- [Precision](Docs/precision.md) - conversion precision and test value guidelines +- [Serialization](Docs/serialization.md), [String Formatting](Docs/string-formatting.md), [Saving to Database](Docs/saving-to-database.md) +- [Upgrade Guides](Docs/README.md#upgrade-guides) for major version migrations + +## Pull request reviews + +### Adding new quantities or units + +See `.agents/criteria-for-adding-quantities-and-units.md` for instructions on adding new quantities or units to ensure they are widely used and well defined. diff --git a/Artifacts/Nugets/.gitkeep b/Artifacts/Nugets/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Artifacts/Nugets/.gitkeep @@ -0,0 +1 @@ + diff --git a/Build/build-functions.psm1 b/Build/build-functions.psm1 index 66bd3e1c4d..eaa437e7b0 100644 --- a/Build/build-functions.psm1 +++ b/Build/build-functions.psm1 @@ -1,34 +1,27 @@ -$root = (Resolve-Path "$PSScriptRoot\..").Path -$artifactsDir = "$root\Artifacts" -$nugetOutDir = "$artifactsDir\NuGet" -$logsDir = "$artifactsDir\Logs" -$testReportDir = "$artifactsDir\TestResults" -$testCoverageDir = "$artifactsDir\Coverage" -$toolsDir = "$root\.tools" - -$nuget = "$toolsDir\NuGet.exe" -$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - -# Check if Visual Studio is installed before trying to find MSBuild -if (Test-Path $vswhere) { - $msbuildPath = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -property installationPath 2>$null - - if ($msbuildPath) { - $msbuildx64 = join-path $msbuildPath 'MSBuild\Current\Bin\amd64\MSBuild.exe' - } -} else { - $msbuildPath = $null - $msbuildx64 = $null -} - -import-module $PSScriptRoot\build-pack-nano-nugets.psm1 +$root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$artifactsDir = Join-Path $root "Artifacts" +$localNuGetFeedDir = Join-Path $artifactsDir "Nugets" +$nugetOutDir = Join-Path $artifactsDir "NuGet" +$logsDir = Join-Path $artifactsDir "Logs" +$testReportDir = Join-Path $artifactsDir "TestResults" +$testCoverageDir = Join-Path $artifactsDir "Coverage" +$toolsDir = Join-Path $root ".tools" +$reportGeneratorName = if ([System.Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { "reportgenerator.exe" } else { "reportgenerator" } +$reportGenerator = Join-Path $toolsDir $reportGeneratorName + +Import-Module (Join-Path $PSScriptRoot "test-projects.psm1") -Force +$testProjectPaths = @(Get-TestProjectPaths) function Remove-ArtifactsDir { if (Test-Path $artifactsDir) { write-host -foreground blue "Clean up...`n" - rm $artifactsDir -Recurse -Force -ErrorAction Stop + Remove-Item -LiteralPath $artifactsDir -Recurse -Force -ErrorAction Stop write-host -foreground blue "Clean up...END`n" } + + # NuGet.Config always includes this repository-local source, so it must exist before restore. + New-Item -ItemType Directory -Force $localNuGetFeedDir 1> $null + Set-Content -LiteralPath (Join-Path $localNuGetFeedDir ".gitkeep") -Value "" } function Update-GeneratedCode { @@ -48,90 +41,69 @@ function Start-Build { write-host -foreground blue "Start-Build...END`n" } -function Start-BuildNanoFramework { - write-host -foreground blue "Start-BuildNanoFramework (MSBuild)...`n---" - - # Check prerequisites - if (-not $msbuildx64 -or -not (Test-Path $msbuildx64)) { - write-host -foreground red "ERROR: Cannot build .NET nanoFramework - MSBuild not found." - write-host -foreground yellow "Install Visual Studio with .NET desktop development workload to build NanoFramework projects." - exit 1 - } - - if (-not (Test-Path $nuget)) { - write-host -foreground red "ERROR: NuGet.exe not found at $nuget" - write-host -foreground yellow "Run init.ps1 to download required tools." - exit 1 - } - - write-host -foreground green "Building .NET nanoFramework projects..." - $fileLoggerArg = "/logger:FileLogger,Microsoft.Build;logfile=$logsDir\UnitsNet.NanoFramework.msbuild.log" - - # msbuild does not auto-restore nugets for this project type - write-host "Restoring NuGet packages for NanoFramework..." - & "$nuget" restore "$root\UnitsNet.NanoFramework\GeneratedCode\UnitsNet.nanoFramework.sln" - if ($lastexitcode -ne 0) { - write-host -foreground red "Failed to restore NuGet packages for NanoFramework" - exit 1 - } - - # Build with MSBuild - write-host "Building NanoFramework solution..." - & "$msbuildx64" "$root\UnitsNet.NanoFramework\GeneratedCode\UnitsNet.nanoFramework.sln" /verbosity:minimal /p:Configuration=Release /p:Platform="Any CPU" /p:ContinuousIntegrationBuild=true $fileLoggerArg - if ($lastexitcode -ne 0) { - write-host -foreground red "Failed to build NanoFramework solution" - exit 1 - } - - write-host -foreground blue "Start-BuildNanoFramework...END`n" -} - function Start-Tests { - $projectPaths = @( - "UnitsNet.Tests\UnitsNet.Tests.csproj", - "UnitsNet.NumberExtensions.Tests\UnitsNet.NumberExtensions.Tests.csproj", - "UnitsNet.NumberExtensions.CS14.Tests\UnitsNet.NumberExtensions.CS14.Tests.csproj", - "UnitsNet.Serialization.JsonNet.Tests\UnitsNet.Serialization.JsonNet.Tests.csproj" - ) + Param( + [switch] $SkipCoverage + ) # Parent dir must exist before xunit tries to write files to it new-item -type directory -force $testReportDir 1> $null - new-item -type directory -force $testCoverageDir 1> $null + if (-not $SkipCoverage) { + new-item -type directory -force $testCoverageDir 1> $null + } write-host -foreground blue "Run tests...`n---" - foreach ($projectPath in $projectPaths) { + foreach ($projectPath in $testProjectPaths) { $projectFileNameNoEx = [System.IO.Path]::GetFileNameWithoutExtension($projectPath) - $coverageReportFile = "$testCoverageDir\${projectFileNameNoEx}.coverage.xml" - $projectDir = [System.IO.Path]::GetDirectoryName($projectPath) + $coverageReportFile = Join-Path $testCoverageDir "${projectFileNameNoEx}.coverage.xml" + $projectDir = Join-Path $root ([System.IO.Path]::GetDirectoryName($projectPath)) # dotnet commands (xunit, dotcover) must run in same dir as project push-location $projectDir - # Create coverage report for this test project - & dotnet dotcover test ` - --no-build ` - --logger trx ` - --results-directory "$testReportDir" ` - --dotCoverFilters="+:module=UnitsNet*;-:module=*Tests" ` - --dotCoverOutput="$coverageReportFile" ` - --dcReportType=DetailedXML + # Build validates every target framework. Run tests only on the latest .NET runtime because + # net8.0, net9.0, and net10.0 compile the same code paths. The separate CLR4 workflow tests + # the meaningfully different netstandard2.0 assets on .NET Framework. + if ($SkipCoverage) { + & dotnet test ` + --no-build ` + --framework net10.0 ` + --logger trx ` + --results-directory "$testReportDir" + } + else { + & dotnet tool run dotCover -- cover-dotnet ` + --TargetWorkingDir $projectDir ` + --Output "$coverageReportFile" ` + --ReportType DetailedXML ` + --Filters '+:module=UnitsNet*;-:module=*Tests' ` + --ReturnTargetExitCode ` + -- test ` + --no-build ` + --framework net10.0 ` + --logger trx ` + --results-directory "$testReportDir" + } if ($lastexitcode -ne 0) { exit 1 } pop-location } - # Generate a summarized code coverage report for all test projects - & "$toolsDir/reportgenerator.exe" -reports:"$testCoverageDir/*.coverage.xml" -targetdir:"$testCoverageDir" -reporttypes:HtmlSummary + if (-not $SkipCoverage) { + # Generate a summarized code coverage report for all test projects + & $reportGenerator -reports:"$testCoverageDir/*.coverage.xml" -targetdir:"$testCoverageDir" -reporttypes:HtmlSummary + } write-host -foreground blue "Run tests...END`n" } function Start-PackNugets { $projectPaths = @( - "UnitsNet\UnitsNet.csproj", - "UnitsNet.Serialization.JsonNet\UnitsNet.Serialization.JsonNet.csproj", - "UnitsNet.NumberExtensions\UnitsNet.NumberExtensions.csproj", - "UnitsNet.NumberExtensions.CS14\UnitsNet.NumberExtensions.CS14.csproj" + "UnitsNet/UnitsNet.csproj", + "UnitsNet.Serialization.JsonNet/UnitsNet.Serialization.JsonNet.csproj", + "UnitsNet.Serialization.SystemTextJson/UnitsNet.Serialization.SystemTextJson.csproj", + "UnitsNet.NumberExtensions/UnitsNet.NumberExtensions.csproj", + "UnitsNet.NumberExtensions.CS14/UnitsNet.NumberExtensions.CS14.csproj" ) write-host -foreground blue "Pack nugets (dotnet CLI)...`n---" @@ -140,7 +112,7 @@ function Start-PackNugets { --no-build ` --output $nugetOutDir ` /p:ContinuousIntegrationBuild=true ` - "$root\$projectPath" + (Join-Path $root $projectPath) if ($lastexitcode -ne 0) { exit 1 } } @@ -148,40 +120,24 @@ function Start-PackNugets { write-host -foreground blue "Pack nugets...END`n" } -function Start-PackNugetsNanoFramework { - write-host -foreground blue "Pack NanoFramework nugets (NuGet.exe)...`n---" - - # Check prerequisites - if (-not (Test-Path $nuget)) { - write-host -foreground red "ERROR: NuGet.exe not found at $nuget" - write-host -foreground yellow "Run init.ps1 to download required tools." - exit 1 - } - - write-host -foreground yellow "nanoFramework project not yet supported by dotnet CLI, using nuget.exe instead" - Invoke-BuildNanoNugets - - write-host -foreground blue "Pack NanoFramework nugets...END`n" -} - function Compress-ArtifactsAsZip { write-host -foreground blue "Zip artifacts...`n---" $zipFileName = "UnitsNet.zip" - $tempZipFile = "$root\$zipFileName" - $zipFile = "$artifactsDir\$zipFileName"` + $tempZipFile = Join-Path $root $zipFileName + $zipFile = Join-Path $artifactsDir $zipFileName - rm $tempZipFile -ErrorAction Ignore - rm $zipFile -ErrorAction Ignore + Remove-Item -LiteralPath $tempZipFile -ErrorAction Ignore + Remove-Item -LiteralPath $zipFile -ErrorAction Ignore # Create zip file add-type -assembly "system.io.compression.filesystem" [IO.Compression.ZipFile]::CreateFromDirectory($artifactsDir, $tempZipFile) - mv $tempZipFile $zipFile + Move-Item -LiteralPath $tempZipFile -Destination $zipFile if (-not $?) { write-host -foreground red "Failed to move [$tempZipFile] to [$zipFileName]."; exit 1 } write-host -foreground blue "Zip artifacts...END`n" } -export-modulemember -function Remove-ArtifactsDir, Update-GeneratedCode, Start-Build, Start-BuildNanoFramework, Start-Tests, Start-PackNugets, Start-PackNugetsNanoFramework, Compress-ArtifactsAsZip +export-modulemember -function Remove-ArtifactsDir, Update-GeneratedCode, Start-Build, Start-Tests, Start-PackNugets, Compress-ArtifactsAsZip diff --git a/Build/build-pack-nano-nugets.psm1 b/Build/build-pack-nano-nugets.psm1 deleted file mode 100644 index b2ed5c3e79..0000000000 --- a/Build/build-pack-nano-nugets.psm1 +++ /dev/null @@ -1,14 +0,0 @@ -$root = (Resolve-Path "$PSScriptRoot\..").Path -$nugetOutDir = "$root\Artifacts\NuGet" -$toolsDir = "$root\.tools" -$nuget = "$toolsDir\NuGet.exe" -$nugetsToProcess = (Get-ChildItem -Path "$root\UnitsNet.NanoFramework\GeneratedCode\" -Filter *.nuspec -r | % { echo $_.FullName }); - -function Invoke-BuildNanoNugets { -Foreach ($nuspecFile in $nugetsToProcess) - { - & $nuget pack "$nuspecFile" -Verbosity detailed -OutputDirectory "$nugetOutDir" - } -} - -export-modulemember -function Invoke-BuildNanoNugets diff --git a/Build/build.ps1 b/Build/build.ps1 index 44185aac0c..dc83fd453b 100644 --- a/Build/build.ps1 +++ b/Build/build.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Build, run tests and pack nugets for all Units.NET projects. .DESCRIPTION @@ -9,7 +9,6 @@ on the master branch. .EXAMPLE powershell ./build.ps1 - powershell ./build.ps1 -IncludeNanoFramework .NOTES Author: Andreas Gullberg Larsen @@ -17,34 +16,33 @@ #> [CmdletBinding()] Param( - [switch] $IncludeNanoFramework - ) + [switch] $SkipTests, + [switch] $SkipCoverage, + [switch] $SkipPack, + [switch] $SkipArchive +) remove-module build-functions -ErrorAction SilentlyContinue -import-module $PSScriptRoot\build-functions.psm1 +import-module (Join-Path $PSScriptRoot "build-functions.psm1") try { - & "$PSScriptRoot/init.ps1" # Ensure tools are downloaded + & "$PSScriptRoot/init.ps1" -SkipCoverageTools:($SkipTests -or $SkipCoverage) Remove-ArtifactsDir Update-GeneratedCode # Build main projects with dotnet CLI (cross-platform) Start-Build - Start-Tests - Start-PackNugets - - # Build NanoFramework if requested (Windows-only, requires Visual Studio) - if ($IncludeNanoFramework) { - write-host -foreground cyan "`n===== Building NanoFramework projects (requires Visual Studio) =====`n" - Start-BuildNanoFramework - Start-PackNugetsNanoFramework + if (-not $SkipTests) { + Start-Tests -SkipCoverage:$SkipCoverage } - else { - write-host -foreground yellow "`nSkipping NanoFramework build. Use -IncludeNanoFramework flag to build NanoFramework projects.`n" + if (-not $SkipPack) { + Start-PackNugets } - Compress-ArtifactsAsZip + if (-not $SkipArchive) { + Compress-ArtifactsAsZip + } } catch { $myError = $_.Exception.ToString() diff --git a/Build/bump-version-UnitsNet.Modular.ps1 b/Build/bump-version-UnitsNet.Modular.ps1 new file mode 100644 index 0000000000..603b2b911f --- /dev/null +++ b/Build/bump-version-UnitsNet.Modular.ps1 @@ -0,0 +1,80 @@ +<# .SYNOPSIS + Creates an annotated UnitsNet.Modular release tag with a bumped version. +.DESCRIPTION + Finds the nearest reachable UnitsNet.Modular/* tag, bumps its minor, patch, or prerelease suffix, + and creates an annotated tag on HEAD. MinVer uses that tag to version UnitsNet.Modular. + + Minor and patch bumps remove any prerelease suffix, matching the existing UnitsNet version scripts. + A suffix bump increments the final numeric prerelease identifier, for example alpha.1 to alpha.2. + After a stable tag, a suffix bump starts the next patch prerelease at alpha.1, matching MinVer's + post-release version range. +.PARAMETER Bump + The semantic version component to bump: minor, patch, or suffix. +.EXAMPLE + ./Build/bump-version-UnitsNet.Modular.ps1 -Bump suffix +.EXAMPLE + ./Build/bump-version-UnitsNet.Modular.ps1 -Bump minor -WhatIf +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateSet("minor", "patch", "suffix")] + [string] $Bump +) + +$ErrorActionPreference = "Stop" +$tagPrefix = "UnitsNet.Modular/" +$repositoryRoot = Resolve-Path "$PSScriptRoot\.." + +Remove-Module set-version -ErrorAction Ignore +Import-Module "$PSScriptRoot\set-version.psm1" + +function Invoke-Git([string[]] $Arguments) { + $output = & git -C $repositoryRoot @Arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "git $([string]::Join(' ', $Arguments)) failed:`n$([string]::Join([Environment]::NewLine, $output))" + } + + return $output +} + +$status = Invoke-Git @("status", "--porcelain", "--untracked-files=normal") +if ($status) { + throw "The working tree must be clean before tagging a release." +} + +$latestTag = [string](Invoke-Git @( + "describe", + "--tags", + "--abbrev=0", + "--match", + "$tagPrefix*", + "HEAD" +)) +$latestTag = $latestTag.Trim() + +if (!$latestTag.StartsWith($tagPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw "Latest tag '$latestTag' does not start with '$tagPrefix'." +} + +$currentVersion = $latestTag.Substring($tagPrefix.Length) +if ($Bump -eq "suffix" -and !$currentVersion.Contains("-")) { + $nextPatchVersion = Get-BumpedSemanticVersion $currentVersion "patch" + $newVersion = Get-BumpedSemanticVersion "$nextPatchVersion-alpha.0" "suffix" "alpha.0" +} +else { + $newVersion = Get-BumpedSemanticVersion $currentVersion $Bump "alpha.0" +} +$newTag = "$tagPrefix$newVersion" + +& git -C $repositoryRoot rev-parse --verify --quiet "refs/tags/$newTag" *> $null +if ($LASTEXITCODE -eq 0) { + throw "Tag '$newTag' already exists." +} + +$message = "UnitsNet.Modular $newVersion" +if ($PSCmdlet.ShouldProcess("HEAD", "Create annotated tag '$newTag'")) { + Invoke-Git @("tag", "--annotate", $newTag, "--message", $message, "HEAD") | Out-Null + Write-Host "Created annotated tag $newTag" + Write-Host "Push it with: git push origin $newTag" +} diff --git a/Build/bump-version-json.bat b/Build/bump-version-json.bat index c8062bf7c9..4f5953987b 100644 --- a/Build/bump-version-json.bat +++ b/Build/bump-version-json.bat @@ -1,10 +1,10 @@ @echo off -rem This scripts increases the version of nugets: UnitsNet.Serialization.JsonNet. +rem This script increases the version of both UnitsNet serialization packages. rem The change is committed and tagged locally, but must be pushed to origin/master to take effect. rem Only contributors with write access can perform this directly to master, others must perform via pull request. SET scriptdir=%~dp0 -echo Bump version UnitsNet.Serialization.JsonNet nuget: +echo Bump version UnitsNet serialization packages: echo. echo 1: minor 4.90.0 to 4.91.0 echo 2: patch 4.90.0 to 4.90.1 diff --git a/Build/bump-version-json.sh b/Build/bump-version-json.sh index 1127db832f..576b6dfa79 100755 --- a/Build/bump-version-json.sh +++ b/Build/bump-version-json.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Increments version of nuget UnitNets.Serialization.JsonNet. +# Increments the version of both UnitsNet serialization packages. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set_version_script="$script_dir/set-version-UnitsNet.Serialization.JsonNet.ps1" diff --git a/Build/bump-version.bat b/Build/bump-version.bat index 192f556e77..8a6cf12bb3 100644 --- a/Build/bump-version.bat +++ b/Build/bump-version.bat @@ -1,10 +1,10 @@ @echo off -rem This scripts increases the version of nugets: UnitsNet, UnitsNet.NumberExtensions. +rem This script increases the version of UnitsNet and both UnitsNet.NumberExtensions packages. rem The change is committed and tagged locally, but must be pushed to origin/master to take effect. rem Only contributors with write access can perform this directly to master, others must perform via pull request. SET scriptdir=%~dp0 -echo Bump version UnitsNet and UnitsNet.NumberExtensions: +echo Bump version UnitsNet and both UnitsNet.NumberExtensions packages: echo. echo 1: minor 4.90.0 to 4.91.0 echo 2: patch 4.90.0 to 4.90.1 diff --git a/Build/bump-version.sh b/Build/bump-version.sh index b94f45e694..8a0b4ca94f 100755 --- a/Build/bump-version.sh +++ b/Build/bump-version.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Increments version of nugets UnitNets, UnitsNet.NumberExtensions. +# Increments version of UnitsNet and both UnitsNet.NumberExtensions packages. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set_version_script="$script_dir/set-version-UnitsNet.ps1" diff --git a/Build/clean.ps1 b/Build/clean.ps1 index 5136a40f69..f900b18337 100644 --- a/Build/clean.ps1 +++ b/Build/clean.ps1 @@ -3,6 +3,7 @@ Set-Strictmode -version latest $root = (Resolve-Path "$PSScriptRoot\..").Path $artifactsDir = "$root\Artifacts" +$localNuGetFeedDir = Join-Path $artifactsDir "Nugets" $toolsDir = "$root\.tools" Write-Host -Foreground Blue "Delete .tools" @@ -11,6 +12,10 @@ Remove-Item -Recurse -Force -ErrorAction Ignore "$toolsDir" Write-Host -Foreground Blue "Delete Artifacts" Remove-Item -Recurse -Force -ErrorAction Ignore "$artifactsDir" +# NuGet.Config always includes this repository-local source, so it must exist before restore. +New-Item -ItemType Directory -Force $localNuGetFeedDir 1> $null +Set-Content -LiteralPath (Join-Path $localNuGetFeedDir ".gitkeep") -Value "" + Write-Host -Foreground Blue "Delete dirs: bin, obj" [int]$deleteCount = 0 diff --git a/Build/init.ps1 b/Build/init.ps1 index e96fffffc9..dd37d5e538 100644 --- a/Build/init.ps1 +++ b/Build/init.ps1 @@ -1,123 +1,29 @@ -# Don't allow using undeclared variables +# Don't allow using undeclared variables +Param( + [switch] $SkipCoverageTools +) + Set-Strictmode -version latest -$root = (Resolve-Path "$PSScriptRoot\..").Path -$nugetPath = "$root/.tools/NuGet.exe" +$root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$toolsDir = Join-Path $root ".tools" +$reportGeneratorName = if ([System.Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { "reportgenerator.exe" } else { "reportgenerator" } +$reportGenerator = Join-Path $toolsDir $reportGeneratorName Write-Host -Foreground Blue "Initializing..." -# Ensure temp dir exists -$tempDir = "$root/.tools/temp_init" -[system.io.Directory]::CreateDirectory($tempDir) | out-null - -# Report generator for unit test coverage reports. -if (-not (Test-Path "$root/.tools/reportgenerator.exe")) { - Write-Host -Foreground Blue "Install dotnet-reportgenerator-globaltool..." - dotnet tool install dotnet-reportgenerator-globaltool --tool-path .tools - Write-Host -Foreground Green "✅ Installed dotnet-reportgenerator-globaltool" -} - -# NuGet.exe for non-SDK style projects, like UnitsNet.nanoFramework. -if (-not (Test-Path "$nugetPath")) { - Write-Host -Foreground Blue "Downloading NuGet.exe..." - Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nugetPath - Write-Host -Foreground Green "✅ Downloaded NuGet.exe: $nugetPath" -} - -################################################### -## TODO: OK to remove after moving to AZDO pipeline -$VsWherePath = "${env:PROGRAMFILES(X86)}\Microsoft Visual Studio\Installer\vswhere.exe" - -# Check if Visual Studio is installed -if (Test-Path $VsWherePath) { - $VsPath = $(&$VsWherePath -latest -property installationPath 2>$null) - if ($VsPath) { - $msbuildPath = Join-Path -Path $VsPath -ChildPath "\MSBuild" - Write-Host -Foreground Green "Visual Studio found at: $VsPath" - } else { - Write-Host -Foreground Yellow "Visual Studio not found via vswhere, NanoFramework builds will be skipped" - $VsPath = $null - $msbuildPath = $null - } -} else { - Write-Host -Foreground Yellow "Visual Studio not installed - NanoFramework builds will be skipped" - $VsPath = $null - $msbuildPath = $null -} - -# Install dotnet CLI tools declared in /.config/dotnet-tools.json -pushd $root -dotnet tool restore -popd - -# Install .NET nanoFramework build components -if ($msbuildPath -and !(Test-Path "$msbuildPath/nanoFramework")) { - Write-Host "Installing .NET nanoFramework VS extension..." - - [System.Net.WebClient]$webClient = New-Object System.Net.WebClient - $webClient.Headers.Add("User-Agent", "request") - $webClient.Headers.Add("Accept", "application/vnd.github.v3+json") - - $releaseList = $webClient.DownloadString('https://api.github.com/repos/nanoframework/nf-Visual-Studio-extension/releases?per_page=100') - - if($releaseList -match '\"(?v2022\.\d+\.\d+\.\d+)\"') - { - $vs2022Tag = $Matches.VS2022_version - } - - if($releaseList -match '\"(?v2019\.\d+\.\d+\.\d+)\"') - { - $vs2019Tag = $Matches.VS2019_version +if (-not $SkipCoverageTools) { + # Report generator for unit test coverage reports. + if (-not (Test-Path $reportGenerator)) { + Write-Host -Foreground Blue "Install dotnet-reportgenerator-globaltool..." + dotnet tool install dotnet-reportgenerator-globaltool --tool-path $toolsDir + Write-Host -Foreground Green "✅ Installed dotnet-reportgenerator-globaltool" } - # Find which VS version is installed - $VsWherePath = "${env:PROGRAMFILES(X86)}\Microsoft Visual Studio\Installer\vswhere.exe" - - Write-Output "VsWherePath is: $VsWherePath" - - $VsInstance = $(&$VSWherePath -latest -property displayName 2>$null) - - Write-Output "Latest VS is: $VsInstance" - - # Get extension details according to VS version, starting from VS2022 down to VS2019 - # TODO check if the extension for VS2022 is compatible it VS2026 - if($vsInstance.Contains('2026') -or $vsInstance.Contains('2022')) - { - $extensionUrl = "https://github.com/nanoframework/nf-Visual-Studio-extension/releases/download/$vs2022Tag/nanoFramework.Tools.VS2022.Extension.vsix" - $vsixPath = Join-Path $tempDir "nanoFramework.Tools.VS2022.Extension.zip" - $extensionVersion = $vs2022Tag - } - elseif($vsInstance.Contains('2019')) - { - $extensionUrl = "https://github.com/nanoframework/nf-Visual-Studio-extension/releases/download/$vs2019Tag/nanoFramework.Tools.VS2019.Extension.vsix" - $vsixPath = Join-Path $tempDir "nanoFramework.Tools.VS2019.Extension.zip" - $extensionVersion = $vs2019Tag - } - - Write-Output "Downloading visx..." - - # download VS extension - Write-Host "Download VSIX file from $extensionUrl to $vsixPath" - $webClient.DownloadFile($extensionUrl, $vsixPath) - - $outputPath = "$tempDir\nf-extension" - - $vsixPath = Join-Path -Path $tempDir -ChildPath "nf-extension.zip" - $webClient.DownloadFile($extensionUrl, $vsixPath) - - Write-Host "Extract VSIX file to $outputPath" - Expand-Archive -LiteralPath $vsixPath -DestinationPath $outputPath -Force | Write-Host - - $copyFrom = "$outputPath\`$MSBuild\nanoFramework" - - Write-Host "Copy from $copyFrom to $msbuildPath" - Copy-Item -Path "$copyFrom" -Destination $msbuildPath -Recurse - - Write-Host "Installed VS extension $extensionVersion" + # Install dotnet CLI tools declared in /.config/dotnet-tools.json + pushd $root + dotnet tool restore + popd } -################################################### - -# Cleanup -[system.io.Directory]::Delete($tempDir, $true) | out-null Write-Host -Foreground Green "Initialized." diff --git a/Build/set-version-UnitsNet.Serialization.JsonNet.ps1 b/Build/set-version-UnitsNet.Serialization.JsonNet.ps1 index b5f6d3d888..1d6c6583d0 100644 --- a/Build/set-version-UnitsNet.Serialization.JsonNet.ps1 +++ b/Build/set-version-UnitsNet.Serialization.JsonNet.ps1 @@ -1,5 +1,5 @@ <# .SYNOPSIS - Updates the version of all UnitsNet.Serialiation.JsonNet projects. + Updates the version of all UnitsNet serialization projects. .DESCRIPTION Updates the property of the .csproj project files. .PARAMETER set @@ -48,11 +48,25 @@ Import-Module "$PSScriptRoot\set-version.psm1" $root = Resolve-Path "$PSScriptRoot\.." $paramSet = $PsCmdlet.ParameterSetName -$projFile = "$root\UnitsNet.Serialization.JsonNet\UnitsNet.Serialization.JsonNet.csproj" +$projectFiles = @( + "$root\UnitsNet.Serialization.JsonNet\UnitsNet.Serialization.JsonNet.csproj", + "$root\UnitsNet.Serialization.SystemTextJson\UnitsNet.Serialization.SystemTextJson.csproj" +) # Use project version as base when bumping major/minor/patch -$newVersion = Get-NewProjectVersion $projFile $paramSet $setVersion $bumpVersion +$newVersion = Get-NewProjectVersion $projectFiles[0] $paramSet $setVersion $bumpVersion + +# Reset and stash any other local changes. +$didStash = Invoke-StashPush + +foreach ($projectFile in $projectFiles) { + Set-ProjectVersion $projectFile $newVersion +} -Set-ProjectVersion $projFile $newVersion Invoke-CommitVersionBump "JsonNet" $newVersion Invoke-TagVersionBump "JsonNet" $newVersion + +# Restore any local changes. +if ($didStash) { + Invoke-StashPop +} diff --git a/Build/set-version-UnitsNet.ps1 b/Build/set-version-UnitsNet.ps1 index b4c0e10f65..e296ae0d60 100644 --- a/Build/set-version-UnitsNet.ps1 +++ b/Build/set-version-UnitsNet.ps1 @@ -51,31 +51,22 @@ Import-Module "$PSScriptRoot\set-version.psm1" $root = Resolve-Path "$PSScriptRoot\.." $paramSet = $PsCmdlet.ParameterSetName -$projFile = "$root\UnitsNet\UnitsNet.csproj" -$numberExtensionsProjFile = "$root\UnitsNet.NumberExtensions\UnitsNet.NumberExtensions.csproj" -$nanoFrameworkNuspecGeneratorFile = "$root\CodeGen\Generators\NanoFrameworkGen\NuspecGenerator.cs" -$nanoFrameworkAssemblyInfoFile = "$root\UnitsNet.NanoFramework\GeneratedCode\Properties\AssemblyInfo.cs" +$projectFiles = @( + "$root\UnitsNet\UnitsNet.csproj", + "$root\UnitsNet.NumberExtensions\UnitsNet.NumberExtensions.csproj", + "$root\UnitsNet.NumberExtensions.CS14\UnitsNet.NumberExtensions.CS14.csproj" +) -# Use UnitsNet.Common.props version as base if bumping major/minor/patch -$newVersion = Get-NewProjectVersion $projFile $paramSet $setVersion $bumpVersion +# Use UnitsNet version as base if bumping major/minor/patch +$newVersion = Get-NewProjectVersion $projectFiles[0] $paramSet $setVersion $bumpVersion # Reset and stash any other local changes. $didStash = Invoke-StashPush # Update project files -Set-ProjectVersion $projFile $newVersion -Set-ProjectVersion $numberExtensionsProjFile $newVersion - -# Update AssemblyInfo.cs file for .NET nanoFramework -Set-AssemblyInfoVersion $nanoFrameworkAssemblyInfoFile $newVersion - -# Update codegen and .nuspec files for nanoFramework -Set-NuspecVersion $nanoFrameworkNuspecGeneratorFile $newVersion -Get-ChildItem -Path "$root\UnitsNet.NanoFramework\GeneratedCode" -Include '*.nuspec' -Recurse | - Foreach-object { - Set-NuspecVersion $_.FullName $newVersion - $versionFiles += $_.FullName - } +foreach ($projectFile in $projectFiles) { + Set-ProjectVersion $projectFile $newVersion +} # Git commit and tag Invoke-CommitVersionBump @("UnitsNet") $newVersion diff --git a/Build/set-version-json.sh b/Build/set-version-json.sh index 38910ba879..529550e97d 100755 --- a/Build/set-version-json.sh +++ b/Build/set-version-json.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Sets version of nuget UnitNets.Serialization.JsonNet. +# Sets the version of both UnitsNet serialization packages. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set_version_script="$script_dir/set-version-UnitsNet.Serialization.JsonNet.ps1" diff --git a/Build/set-version.psm1 b/Build/set-version.psm1 index 6b28533c5a..015c5c81d6 100644 --- a/Build/set-version.psm1 +++ b/Build/set-version.psm1 @@ -71,85 +71,86 @@ function Set-NuspecVersion([string] $file, [string] $version) { function Get-BumpedProjectVersion([string] $projectPath, [string] $bumpVersion) { [xml]$projectXml = Get-Content -Path $projectPath - $old = Get-ProjectVersionAndSuffix $projectXml + $oldSemVer = [string]($projectXml.Project.PropertyGroup.Version)[0] + + return Get-BumpedSemanticVersion $oldSemVer $bumpVersion +} + +function Get-BumpedSemanticVersion( + [string] $semanticVersion, + [string] $bumpVersion, + [string] $defaultPreReleaseIdentifiers = "alpha000") { + $match = [regex]::Match( + $semanticVersion, + '^(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)(?:-(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(?[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$') + + if (!$match.Success) { + throw "Unable to parse semantic version '$semanticVersion'." + } + + $preRelease = $match.Groups["prerelease"].Value + foreach ($identifier in $preRelease.Split(".", [StringSplitOptions]::RemoveEmptyEntries)) { + if ($identifier -match '^[0-9]+$' -and $identifier.Length -gt 1 -and $identifier.StartsWith("0")) { + throw "Numeric prerelease identifier '$identifier' in '$semanticVersion' must not contain leading zeroes." + } + } + + $major = [long]$match.Groups["major"].Value + $minor = [long]$match.Groups["minor"].Value + $patch = [long]$match.Groups["patch"].Value switch ($bumpVersion) { "major" { - $newVersion = BumpMajor $old.Version - $newSuffix = "" + return "$($major + 1).0.0" } "minor" { - $newVersion = BumpMinor $old.Version - $newSuffix = "" + return "$major.$($minor + 1).0" } "patch" { - $newVersion = BumpPatch $old.Version - $newSuffix = "" + return "$major.$minor.$($patch + 1)" } "suffix" { - $newVersion = $old.Version - $newSuffix = BumpSuffix $old.Suffix + $newSuffix = BumpSuffix $(if ($preRelease) { "-$preRelease" } else { "" }) $defaultPreReleaseIdentifiers + return "$major.$minor.$patch$newSuffix" } default { throw "Unrecognized 'bumpVersion' argument: $bumpVersion" } } - - $newSemVer = $newVersion.ToString() + $newSuffix - return $newSemVer -} - -function BumpMajor ([Version] $oldVersion) { - return New-Object System.Version -ArgumentList ($oldVersion.Major+1), 0, 0 -} - -function BumpMinor([Version] $oldVersion) { - return New-Object System.Version -ArgumentList $oldVersion.Major, ($oldVersion.Minor+1), 0 -} - -function BumpPatch([Version] $oldVersion) { - return New-Object System.Version -ArgumentList $oldVersion.Major, $oldVersion.Minor, ($oldVersion.Build+1); } -function BumpSuffix([string] $oldSuffix) { +function BumpSuffix( + [string] $oldSuffix, + [string] $defaultPreReleaseIdentifiers = "alpha000") { $oldSuffix = $oldSuffix.Trim() - # A suffix is a dash '-', then a sequcence of word characters followed by an optional number - # Example: - # "" => "-alpha001" - # "-beta" => "-beta001" - # "-beta1"=> "-beta002" - $match = [regex]::Match($oldSuffix, '^-([a-zA-Z]+)(\d+)?$'); - $oldSuffix = $match.Groups[1].Value - if (!$oldSuffix) { - $oldSuffix = "alpha" + $preRelease = $oldSuffix.TrimStart("-") + if (!$preRelease) { + $preRelease = $defaultPreReleaseIdentifiers } - $numberGroup = $match.Groups[2] - - $number = if ($numberGroup.Success) { 1+$match.Groups[2].Value } else { 1 } - - # Use 3 digits for the number "-alpha003", for 999 releases lexically sorted. - return [string]::Format("-{0}{1:D3}", $oldSuffix, $number) -} - -# Returns object with properties: Version, Suffix -function Get-ProjectVersionAndSuffix([xml] $projectXml) { + $identifiers = [Collections.Generic.List[string]]::new() + $identifiers.AddRange([string[]]$preRelease.Split(".")) + $lastIdentifierIndex = $identifiers.Count - 1 + $lastIdentifier = $identifiers[$lastIdentifierIndex] - # Split "1.2.3-alpha" into ["1.2.3", "alpha"] - # Split "1.2.3" into ["1.2.3"] - $oldSemVer = $($projectXml.Project.PropertyGroup.Version)[0] - - $oldSemVerParts = $oldSemVer.Split('-') - $oldVersion = $null - if (-not [Version]::TryParse($oldSemVerParts[0], [ref] $oldVersion)) { throw "Unable to parse old version." } - - $oldSuffix = if ($oldSemVerParts.Length -eq 2) { "-" + $oldSemVerParts[1]} else { "" } - return [PSCustomObject]@{ - PSTypeName = "VersionAndSuffix" - Version = $oldVersion - Suffix = $oldSuffix + if ($lastIdentifier -match '^[0-9]+$') { + $identifiers[$lastIdentifierIndex] = ([long]$lastIdentifier + 1).ToString() + } + elseif ($lastIdentifier -match '^(?.*?)(?[0-9]+)$') { + $numberWidth = $Matches["number"].Length + $number = [long]$Matches["number"] + 1 + $identifiers[$lastIdentifierIndex] = $Matches["prefix"] + $number.ToString("D$numberWidth") } + elseif ($identifiers.Count -eq 1) { + # Preserve the repository's existing alpha001/beta001 suffix convention. + $identifiers[0] += "001" + } + else { + $identifiers.Add("1") + } + + return "-" + [string]::Join(".", $identifiers) } function Resolve-Error ($ErrorRecord=$Error[0]) @@ -169,6 +170,7 @@ function Resolve-Error ($ErrorRecord=$Error[0]) } export-modulemember -function Get-NewProjectVersion, + Get-BumpedSemanticVersion, Invoke-CommitVersionBump, Invoke-TagVersionBump, Set-ProjectVersion, diff --git a/Build/set-version.sh b/Build/set-version.sh index 78f9ffd372..d45f2fa0bf 100755 --- a/Build/set-version.sh +++ b/Build/set-version.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Increments version of nugets UnitNets, UnitsNet.NumberExtensions. +# Sets the version of UnitsNet and both UnitsNet.NumberExtensions packages. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set_version_script="$script_dir/set-version-UnitsNet.ps1" diff --git a/Build/test-projects.psm1 b/Build/test-projects.psm1 new file mode 100644 index 0000000000..04250311ec --- /dev/null +++ b/Build/test-projects.psm1 @@ -0,0 +1,13 @@ +function Get-TestProjectPaths { + return @( + "UnitsNet.Tests/UnitsNet.Tests.csproj", + "UnitsNet.GlobalSetup.DefaultFirst.Tests/UnitsNet.GlobalSetup.DefaultFirst.Tests.csproj", + "UnitsNet.GlobalSetup.Tests/UnitsNet.GlobalSetup.Tests.csproj", + "UnitsNet.NumberExtensions.Tests/UnitsNet.NumberExtensions.Tests.csproj", + "UnitsNet.NumberExtensions.CS14.Tests/UnitsNet.NumberExtensions.CS14.Tests.csproj", + "UnitsNet.Serialization.JsonNet.Tests/UnitsNet.Serialization.JsonNet.Tests.csproj", + "UnitsNet.Serialization.SystemTextJson.Tests/UnitsNet.Serialization.SystemTextJson.Tests.csproj" + ) +} + +Export-ModuleMember -Function Get-TestProjectPaths diff --git a/CLAUDE.md b/CLAUDE.md index 9d426d0d2f..6cb611ccbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,135 +1,5 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +@AGENTS.md -## Project Overview - -UnitsNet is a .NET library that provides strongly-typed physical units and quantities, enabling safe and intuitive unit conversions in code. The library uses code generation from JSON definitions to create type-safe APIs for over 130 physical quantities. - -## Key Commands - -### Build and Test -- **Build project**: `build.bat` or `dotnet build UnitsNet.slnx` -- **Build all targets** (including nanoFramework): `build-all-targets.bat` -- **Run tests**: `test.bat` or `dotnet test UnitsNet.slnx` -- **Run single test**: `dotnet test UnitsNet.Tests --filter "FullyQualifiedName~TestClassName.TestMethodName"` -- **Clean artifacts**: `clean.bat` - -### Code Generation -- **Generate code from JSON definitions**: `generate-code.bat` or `dotnet run --project CodeGen` - - Always run this after modifying any JSON files in `Common/UnitDefinitions/` - - The generator reads 131 JSON definition files and creates C# code - -### Development Workflow -1. Modify unit definitions in `Common/UnitDefinitions/*.json` -2. Run `generate-code.bat` to regenerate C# code -3. Run `build.bat` to compile and test -4. Use `test.bat` for isolated test runs - -## Code Architecture - -### Project Structure -- **UnitsNet/**: Main library with quantity types and units - - `GeneratedCode/`: Auto-generated from JSON definitions (do not edit manually) - - `CustomCode/`: Hand-written code extending generated types -- **UnitsNet.Tests/**: Comprehensive test suite -- **CodeGen/**: Code generation tool that creates C# from JSON definitions -- **Common/UnitDefinitions/**: 131 JSON files defining physical quantities -- **UnitsNet.NumberExtensions/**: Extension methods for numeric types -- **UnitsNet.Serialization.*/**: JSON.NET and System.Text.Json serialization support -- **UnitsNet.NanoFramework/**: Support for embedded .NET nanoFramework - -### Code Generation Process -The project uses a sophisticated code generation system: -1. JSON definitions in `Common/UnitDefinitions/` describe units, conversions, and localizations -2. `CodeGen` project processes these to generate: - - Quantity types (e.g., `Length`, `Mass`) - - Unit enums (e.g., `LengthUnit`, `MassUnit`) - - Conversion logic and unit abbreviations -3. Generated code goes to `*/GeneratedCode/` folders -4. Custom code in `*/CustomCode/` extends generated types - -### Key Classes and Patterns -- **IQuantity**: Base interface for all quantity types -- **Quantity**: Static class for dynamic quantity operations -- **UnitConverter**: Handles conversions between units -- **QuantityParser/UnitParser**: Parse strings to quantities/units -- **UnitsNetSetup**: Configuration singleton - -### Adding or Modifying Units -1. Edit or create JSON file in `Common/UnitDefinitions/` -2. Follow conversion function guidelines in [Docs/adding-a-new-unit.md](Docs/adding-a-new-unit.md): - - Use multiplication for `FromUnitToBaseFunc` - - Use division for `FromBaseToUnitFunc` - - Prefer scientific notation (1e3, 1e-5) - - Use exact constituent constants instead of pre-computed decimals -3. Run `generate-code.bat` -4. Add tests if needed - -## Important Conventions - -### Coding Standards -- Follow `.editorconfig` specifications -- Use ReSharper settings in `UnitsNet.sln.DotSettings` -- Treat warnings as errors (except obsolete warnings) -- Add file headers to new files - -### Unit Definition Rules -- Base units are chosen for each quantity (e.g., meter for Length) -- All conversions go through the base unit -- Use superscript in abbreviations: cm², m³ -- Compound units format: N·m (dot), km/h (slash) - -### Testing -- Test class naming: `Tests` -- Test method naming: `__` -- Tests accept error margin of 1E-5 for most units - -## Special Considerations - -### .NET nanoFramework Support -- Separate projects for nanoFramework compatibility -- Use `build-all-targets.bat` to include nanoFramework builds -- Limited feature set compared to full .NET - -### Performance -- Conversion functions are compiled to delegates for performance -- All conversions go through base units (potential for small errors) -- Precision goal is 1E-5 for most units - -### Localization -- Unit abbreviations support multiple cultures -- JSON definitions include translations for various languages -- Default culture: Thread.CurrentCulture, fallback to en-US - -## Common Tasks - -### Find specific quantity or unit implementation -- Quantity types: `UnitsNet/GeneratedCode/Quantities/*.g.cs` -- Unit enums: `UnitsNet/GeneratedCode/Units/*.g.cs` -- Custom extensions: `UnitsNet/CustomCode/Quantities/*.extra.cs` -- Unit definitions: `Common/UnitDefinitions/*.json` - -### Debug code generation -- Generator entry: `CodeGen/Program.cs` -- Generator logic: `CodeGen/Generators/` -- Enable verbose logging: Check Serilog configuration in Program.cs - -### Run performance benchmarks -- Execute: `dotnet run -c Release --project UnitsNet.Benchmark` -- Results saved to `Artifacts/` folder - -## Documentation - -All contributor and user documentation lives in [Docs/](Docs/README.md), including: -- [Adding a New Unit](Docs/adding-a-new-unit.md) — step-by-step guide with JSON schema conventions -- [Adding Operator Overloads](Docs/adding-operator-overloads.md) -- [Precision](Docs/precision.md) — conversion precision and test value guidelines -- [Serialization](Docs/serialization.md), [String Formatting](Docs/string-formatting.md), [Saving to Database](Docs/saving-to-database.md) -- [Upgrade Guides](Docs/README.md#upgrade-guides) for major version migrations - -## Pull request reviews - -### Adding new quantities or units - -See `.claude/criteria-for-adding-quantities-and-units.md` for instructions on adding new quantities or units to ensure they are widely used and well defined. +Follow the shared repository guidance in `AGENTS.md`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2645925997..27eabe43ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ This is to help grow the number of people that can contribute to the project and * Test class: Use `Tests` suffix for the type you are testing, such as `UnitSystemTests` * Test method: `__` (`Parse_AmbiguousUnits_ThrowsException`) * If there are many tests for a single method, you can wrap those in an inner class named the same as the method and then you can skip that part of the test method names +* Test conversion values should have at least 7 significant digits (`double` supports ~15-17, so anything beyond 16 is not useful) ## Unit Definitions (.JSON) diff --git a/CodeGen/CodeGen.csproj b/CodeGen/CodeGen.csproj index da0c0ddb03..c0b882ce31 100644 --- a/CodeGen/CodeGen.csproj +++ b/CodeGen/CodeGen.csproj @@ -20,6 +20,7 @@ + diff --git a/CodeGen/Exceptions/UnitsNetCodeGenException.cs b/CodeGen/Exceptions/UnitsNetCodeGenException.cs index e25340993a..b3aab05d5d 100644 --- a/CodeGen/Exceptions/UnitsNetCodeGenException.cs +++ b/CodeGen/Exceptions/UnitsNetCodeGenException.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; diff --git a/CodeGen/Generators/NanoFrameworkGen/NuspecGenerator.cs b/CodeGen/Generators/NanoFrameworkGen/NuspecGenerator.cs deleted file mode 100644 index c5fbe836a0..0000000000 --- a/CodeGen/Generators/NanoFrameworkGen/NuspecGenerator.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using CodeGen.JsonTypes; - -namespace CodeGen.Generators.NanoFrameworkGen -{ - class NuspecGenerator : GeneratorBase - { - private readonly Quantity _quantity; - private readonly string _mscorlibNuGetVersion; - private readonly string _mathNuGetVersion; - - public NuspecGenerator( - Quantity quantity, - string mscorlibNuGetVersion, - string mathNuGetVersion) - { - _quantity = quantity ?? throw new ArgumentNullException(nameof(quantity)); - _mscorlibNuGetVersion = mscorlibNuGetVersion; - _mathNuGetVersion = mathNuGetVersion; - } - - public string Generate() - { - Writer.WL($@" - - - UnitsNet.nanoFramework.{_quantity.Name} - 6.0.0-pre019 - Units.NET {_quantity.Name} - nanoFramework - Andreas Gullberg Larsen,nanoframework - UnitsNet - MIT-0 - https://github.com/angularsen/UnitsNet - false - Adds {_quantity.Name} units for Units.NET on .NET nanoFramework. For .NET or .NET Core, use UnitsNet instead. - https://raw.githubusercontent.com/angularsen/UnitsNet/ce85185429be345d77eb2ce09c99d59cc9ab8aed/Docs/Images/logo-32.png - - - Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). - en-US - nanoframework {_quantity.Name.ToLower()} unit units quantity quantities measurement si metric imperial abbreviation abbreviations convert conversion parse immutable - - "); - - if (NanoFrameworkGenerator.ProjectsRequiringMath.Contains(_quantity.Name)) - { - Writer.WL($@" - "); - } - - Writer.WL($@" - - - - - -"); - - return Writer.ToString(); - } - } -} diff --git a/CodeGen/Generators/NanoFrameworkGen/ProjectGenerator.cs b/CodeGen/Generators/NanoFrameworkGen/ProjectGenerator.cs deleted file mode 100644 index f07787513d..0000000000 --- a/CodeGen/Generators/NanoFrameworkGen/ProjectGenerator.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using CodeGen.Helpers; -using CodeGen.JsonTypes; - -namespace CodeGen.Generators.NanoFrameworkGen -{ - class ProjectGenerator: GeneratorBase - { - private readonly Quantity _quantity; - private readonly NanoFrameworkVersions _versions; - - public ProjectGenerator(Quantity quantity, NanoFrameworkVersions versions) - { - _quantity = quantity ?? throw new ArgumentNullException(nameof(quantity)); - _versions = versions; - } - - public string Generate() - { - Writer.WL($@" - - - $(MSBuildExtensionsPath)\nanoFramework\v1.0\ - - - - Debug - AnyCPU - {{11A8DD76-328B-46DF-9F39-F559912D0360}};{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}} - {HashGuid.ToHashGuid(_quantity.Name):B} - Library - Properties - 512 - UnitsNet - UnitsNet.{_quantity.Name} - v1.0 - bin\$(Configuration)\$(AssemblyName).xml - - - - - - - - - - ..\packages\nanoFramework.CoreLibrary.{_versions.MscorlibNugetVersion}\lib\mscorlib.dll - True - True - "); - - if (NanoFrameworkGenerator.ProjectsRequiringMath.Contains(_quantity.Name)) - { - Writer.WL($@" - - ..\packages\nanoFramework.System.Math.{_versions.MathNugetVersion}\lib\System.Math.dll - True - True - "); - } - - Writer.WL(@" - - - - - - - - - - -"); - - return Writer.ToString(); - } - } -} diff --git a/CodeGen/Generators/NanoFrameworkGen/PropertyGenerator.cs b/CodeGen/Generators/NanoFrameworkGen/PropertyGenerator.cs deleted file mode 100644 index 6973936633..0000000000 --- a/CodeGen/Generators/NanoFrameworkGen/PropertyGenerator.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace CodeGen.Generators.NanoFrameworkGen -{ - class PropertyGenerator : GeneratorBase - { - private readonly string _version; - - public PropertyGenerator(string version) - { - _version = version; - } - - public string Generate() - { - Writer.WL(GeneratedFileHeader); - Writer.W($@"using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle(""UnitsNet"")] -[assembly: AssemblyDescription(""Get all the common units of measurement and the conversions between them. It is light-weight and thoroughly tested."")] -[assembly: AssemblyConfiguration("""")] -[assembly: AssemblyCompany(""Andreas Gullberg Larsen"")] -[assembly: AssemblyProduct(""nanoFramework UnitsNet"")] -[assembly: AssemblyCopyright(""Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com)."")] -[assembly: AssemblyTrademark("""")] -[assembly: AssemblyCulture("""")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion(""{_version}"")] -[assembly: AssemblyFileVersion(""{_version}"")] - -////////////////////////////////////////////////// -// This assembly doens't require native support // -[assembly: AssemblyNativeVersion(""0.0.0.0"")] -////////////////////////////////////////////////// -"); - return Writer.ToString(); - } - } -} diff --git a/CodeGen/Generators/NanoFrameworkGen/QuantityGenerator.cs b/CodeGen/Generators/NanoFrameworkGen/QuantityGenerator.cs deleted file mode 100644 index 04d8acad5f..0000000000 --- a/CodeGen/Generators/NanoFrameworkGen/QuantityGenerator.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System; -using CodeGen.Helpers; -using CodeGen.JsonTypes; - -namespace CodeGen.Generators.NanoFrameworkGen -{ - internal class QuantityGenerator : GeneratorBase - { - private readonly Quantity _quantity; - private readonly string _unitEnumName; - - public QuantityGenerator(Quantity quantity) - { - _quantity = quantity ?? throw new ArgumentNullException(nameof(quantity)); - _unitEnumName = $"{quantity.Name}Unit"; - } - - public string Generate() - { - // Auto generated header - Writer.WL(GeneratedFileHeader); - // Usings, properties - Writer.WL($@"using System; -using UnitsNet.Units; - -namespace UnitsNet -{{"); - Writer.WL($@" - /// - /// - /// {_quantity.XmlDocSummary} - /// "); - - Writer.WLCondition(_quantity.XmlDocRemarks.HasText(), $@" - /// - /// {_quantity.XmlDocRemarks} - /// "); - Writer.WLIfText(1, GetObsoleteAttributeOrNull(_quantity)); - - Writer.WL($@" - public struct {_quantity.Name} - {{ - /// - /// The numeric value this quantity was constructed with. - /// - private readonly double _value; - - /// - /// The unit this quantity was constructed with. - /// - private readonly {_unitEnumName} _unit; - - /// - /// The numeric value this quantity was constructed with. - /// - public double Value => _value; - - /// - public {_unitEnumName} Unit => _unit; -"); - - // Constructor and static properties - Writer.WL($@" /// - /// Creates the quantity with the given numeric value and unit. - /// - /// The numeric value to construct this quantity with. - /// The unit representation to construct this quantity with. - public {_quantity.Name}(double value, {_unitEnumName} unit) - {{ - _value = value; - _unit = unit; - }} - - /// - /// The base unit of {_quantity.Name}, which is Second. All conversions go via this value. - /// - public static {_unitEnumName} BaseUnit {{ get; }} = {_unitEnumName}.{_quantity.BaseUnit}; - - /// - /// Represents the largest possible value of {_quantity.Name}. - /// - public static {_quantity.Name} MaxValue {{ get; }} = new {_quantity.Name}(double.MaxValue, BaseUnit); - - /// - /// Represents the smallest possible value of {_quantity.Name}. - /// - public static {_quantity.Name} MinValue {{ get; }} = new {_quantity.Name}(double.MinValue, BaseUnit); - - /// - /// Gets an instance of this quantity with a value of 0 in the base unit Second. - /// - public static {_quantity.Name} Zero {{ get; }} = new {_quantity.Name}(0, BaseUnit);"); - - GenerateConversionProperties(); - GenerateStaticFactoryMethods(); - GenerateConversionMethods(); - - Writer.WL(@" - } -} -"); - - return Writer.ToString(); - } - - private void GenerateConversionProperties() - { - Writer.WL(@" - #region Conversion Properties -"); - foreach (Unit unit in _quantity.Units) - { - if (unit.SkipConversionGeneration) continue; - - Writer.WL($@" - /// - /// Gets a value of this quantity converted into - /// "); - Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit)); - Writer.WL($@" - public double {unit.PluralName} => As({_unitEnumName}.{unit.SingularName}); -"); - } - - Writer.WL(@" - - #endregion -"); - } - - private void GenerateStaticFactoryMethods() - { - Writer.WL(@" - #region Static Factory Methods -"); - foreach (Unit unit in _quantity.Units) - { - if (unit.SkipConversionGeneration) continue; - - var valueParamName = unit.PluralName.ToLowerInvariant(); - Writer.WL($@" - /// - /// Creates a from . - /// "); - Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit)); - Writer.WL($@" - public static {_quantity.Name} From{unit.PluralName}(double {valueParamName}) => new {_quantity.Name}({valueParamName}, {_unitEnumName}.{unit.SingularName}); -"); - } - - Writer.WL($@" - /// - /// Dynamically convert from value and unit enum to . - /// - /// Value to convert from. - /// Unit to convert from. - /// {_quantity.Name} unit value. - public static {_quantity.Name} From(double value, {_unitEnumName} fromUnit) - {{ - return new {_quantity.Name}(value, fromUnit); - }} - - #endregion -"); - } - - private void GenerateConversionMethods() - { - Writer.WL($@" - #region Conversion Methods - - /// - /// Convert to the unit representation . - /// - /// Value converted to the specified unit. - public double As({_unitEnumName} unit) => GetValueAs(unit); - - /// - /// Converts this {_quantity.Name} to another {_quantity.Name} with the unit representation . - /// - /// A {_quantity.Name} with the specified unit. - public {_quantity.Name} ToUnit({_unitEnumName} unit) - {{ - var convertedValue = GetValueAs(unit); - return new {_quantity.Name}(convertedValue, unit); - }} - - /// - /// Converts the current value + unit to the base unit. - /// This is typically the first step in converting from one unit to another. - /// - /// The value in the base unit representation. - private double GetValueInBaseUnit() - {{ - return Unit switch - {{"); - foreach (Unit unit in _quantity.Units) - { - var func = unit.FromUnitToBaseFunc.Replace("{x}", "_value"); - Writer.WL($@" - {_unitEnumName}.{unit.SingularName} => {func},"); - } - - Writer.WL($@" - _ => throw new NotImplementedException($""Can't convert {{Unit}} to base units."") - }}; - }} - - private double GetValueAs({_unitEnumName} unit) - {{ - if (Unit == unit) - return _value; - - var baseUnitValue = GetValueInBaseUnit(); - - return unit switch - {{"); - foreach (Unit unit in _quantity.Units) - { - var func = unit.FromBaseToUnitFunc.Replace("{x}", "baseUnitValue"); - Writer.WL($@" - {_unitEnumName}.{unit.SingularName} => {func},"); - } - - Writer.WL(@" - _ => throw new NotImplementedException($""Can't convert {Unit} to {unit}."") - }; - } - - #endregion"); - } - - /// - private static string? GetObsoleteAttributeOrNull(Quantity quantity) => GetObsoleteAttributeOrNull(quantity.ObsoleteText); - - /// - private static string? GetObsoleteAttributeOrNull(Unit unit) => GetObsoleteAttributeOrNull(unit.ObsoleteText); - - /// - /// Returns the Obsolete attribute if ObsoleteText has been defined on the JSON input - otherwise returns empty string - /// It is up to the consumer to wrap any padding/new lines in order to keep to correct indentation formats - /// - private static string? GetObsoleteAttributeOrNull(string? obsoleteText) => string.IsNullOrWhiteSpace(obsoleteText) - ? null - : $"[Obsolete(\"{obsoleteText}\")]"; - - } -} diff --git a/CodeGen/Generators/NanoFrameworkGen/SolutionGenerator.cs b/CodeGen/Generators/NanoFrameworkGen/SolutionGenerator.cs deleted file mode 100644 index ac919c0574..0000000000 --- a/CodeGen/Generators/NanoFrameworkGen/SolutionGenerator.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Text; -using CodeGen.Helpers; -using CodeGen.JsonTypes; - -namespace CodeGen.Generators.NanoFrameworkGen -{ - class SolutionGenerator:GeneratorBase - { - private readonly Quantity[] _quantities; - private readonly Guid _globalGuid = new("d608a2b1-6ead-4383-a205-ad1ce69d9ef7"); // Randomly generated guids. - private readonly Guid _solutionGuid = new("43971d92-3663-4f28-82ac-e63ce06ba1a3"); - - public SolutionGenerator(Quantity[] quantities) - { - _quantities = quantities; - } - - public string Generate() - { - StringBuilder sb = new(); - Writer.WL($@"Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30413.136 -MinimumVisualStudioVersion = 10.0.40219.1"); - - foreach (var quantity in _quantities) - { - var projectGuid = HashGuid.ToHashGuid(quantity.Name); - Writer.WL($@" -Project(""{_globalGuid:B}"") = ""{quantity.Name}"", ""{quantity.Name}\{quantity.Name}.nfproj"", ""{projectGuid:B}"" -EndProject"); - sb.Append($"{{{projectGuid}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n"); - sb.Append($"{{{projectGuid}}}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n"); - sb.Append($"{{{projectGuid}}}.Debug|Any CPU.Deploy.0 = Debug|Any CPU\r\n"); - sb.Append($"{{{projectGuid}}}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n"); - sb.Append($"{{{projectGuid}}}.Release|Any CPU.Build.0 = Release|Any CPU\r\n"); - sb.Append($"{{{projectGuid}}}.Release|Any CPU.Deploy.0 = Release|Any CPU\r\n"); - } - - Writer.WL(@"Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution"); - - Writer.WL(sb.ToString()); - - Writer.WL($@" EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {_solutionGuid:B} - EndGlobalSection -EndGlobal -"); - return Writer.ToString(); - } - } -} diff --git a/CodeGen/Generators/NanoFrameworkGen/UnitTypeGenerator.cs b/CodeGen/Generators/NanoFrameworkGen/UnitTypeGenerator.cs deleted file mode 100644 index 5e485b9cda..0000000000 --- a/CodeGen/Generators/NanoFrameworkGen/UnitTypeGenerator.cs +++ /dev/null @@ -1,67 +0,0 @@ -using CodeGen.Helpers; -using CodeGen.Helpers.UnitEnumValueAllocation; -using CodeGen.JsonTypes; - -namespace CodeGen.Generators.NanoFrameworkGen -{ - internal class UnitTypeGenerator : GeneratorBase - { - private readonly Quantity _quantity; - private readonly UnitEnumNameToValue _unitEnumNameToValue; - private readonly string _unitEnumName; - - public UnitTypeGenerator(Quantity quantity, UnitEnumNameToValue unitEnumNameToValue) - { - _quantity = quantity; - _unitEnumNameToValue = unitEnumNameToValue; - _unitEnumName = $"{quantity.Name}Unit"; - } - - public string Generate() - { - Writer.WL(GeneratedFileHeader); - Writer.WL($@" -// ReSharper disable once CheckNamespace -namespace UnitsNet.Units -{{ - // Disable missing XML comment warnings for the generated unit enums. - #pragma warning disable 1591 - - public enum {_unitEnumName} - {{"); - foreach (Unit unit in _quantity.Units) - { - if (unit.XmlDocSummary.HasText()) - { - Writer.WL(); - Writer.WL($@" - /// - /// {unit.XmlDocSummary} - /// "); - } - - if (unit.XmlDocRemarks.HasText()) - { - Writer.WL($@" - /// {unit.XmlDocRemarks}"); - } - - Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit.ObsoleteText)); - Writer.WL($@" - {unit.SingularName} = {_unitEnumNameToValue[unit.SingularName]},"); - } - - Writer.WL($@" - }} - - #pragma warning restore 1591 -}}"); - return Writer.ToString(); - } - - private static string? GetObsoleteAttributeOrNull(string? obsoleteText) => - string.IsNullOrWhiteSpace(obsoleteText) ? - null : - $"[System.Obsolete(\"{obsoleteText}\")]"; - } -} diff --git a/CodeGen/Generators/NanoFrameworkGenerator.cs b/CodeGen/Generators/NanoFrameworkGenerator.cs deleted file mode 100644 index 19d8cab43c..0000000000 --- a/CodeGen/Generators/NanoFrameworkGenerator.cs +++ /dev/null @@ -1,394 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using CodeGen.Generators.NanoFrameworkGen; -using CodeGen.Helpers; -using CodeGen.Helpers.UnitEnumValueAllocation; -using CodeGen.JsonTypes; -using NuGet.Common; -using Serilog; -using ILogger = NuGet.Common.ILogger; - -namespace CodeGen.Generators -{ - /// - /// Code generator for nanoFramework - /// Will generate 1 nanoFramework project per unit, a common property folder and a common package file - /// - internal static class NanoFrameworkGenerator - { - /// - /// These projects require inclusion of Math NuGet package. - /// - internal static readonly List ProjectsRequiringMath = new() - { - "Angle", - "Frequency", - "Pressure", - "Turbidity", - "WarpingMomentOfInertia" - }; - - /// - /// Create the root folder NanoFramework - /// Create all the quantities unit and quantities file - /// Create all individual nanoFramework projects - /// Create common package file - /// Create common properties file - /// - /// The root directory - /// The quantities to create - /// - public static void Generate(string rootDir, Quantity[] quantities, QuantityNameToUnitEnumValues quantityNameToUnitEnumValues) - { - // get latest version of .NET nanoFramework mscorlib - ILogger logger = NullLogger.Instance; - - NanoFrameworkVersions versions = ParseCurrentNanoFrameworkVersions(rootDir); - - logger.LogInformation($"Referencing nanoFramework.CoreLibrary {versions.MscorlibNugetVersion}"); - logger.LogInformation($"Referencing nanoFramework.System.Math {versions.MathNugetVersion}"); - - var outputDir = Path.Combine(rootDir, "UnitsNet.NanoFramework", "GeneratedCode"); - var outputQuantities = Path.Combine(outputDir, "Quantities"); - var outputUnits = Path.Combine(outputDir, "Units"); - var outputProperties = Path.Combine(outputDir, "Properties"); - - // Ensure output directories exist - Directory.CreateDirectory(outputQuantities); - Directory.CreateDirectory(outputUnits); - Directory.CreateDirectory(outputProperties); - - var lengthNuspecFile = Path.Combine(outputDir, "Length", "UnitsNet.NanoFramework.Length.nuspec"); - var projectVersion = ParseVersion(File.ReadAllText(lengthNuspecFile), - new Regex(@"(?[\d.]+)(?-[a-z\d]+)?<\/version>", RegexOptions.IgnoreCase), - "projectVersion"); - - foreach (Quantity quantity in quantities) - { - var projectPath = Path.Combine(outputDir, quantity.Name); - Directory.CreateDirectory(projectPath); - - GeneratePackageConfig( - projectPath, - quantity.Name, - versions.MscorlibNugetVersion, - versions.MathNugetVersion); - - GenerateNuspec( - projectPath, - quantity, - versions.MscorlibNugetVersion, - versions.MathNugetVersion); - - UnitEnumNameToValue unitEnumValues = quantityNameToUnitEnumValues[quantity.Name]; - - GenerateUnitType(quantity, Path.Combine(outputUnits, $"{quantity.Name}Unit.g.cs"), unitEnumValues); - GenerateQuantity(quantity, Path.Combine(outputQuantities, $"{quantity.Name}.g.cs")); - GenerateProject(quantity, Path.Combine(projectPath, $"{quantity.Name}.nfproj"), versions); - - Log.Information("✅ {Quantity} (nanoFramework)", quantity.Name); - } - Log.Information(""); - - GenerateProperties(Path.Combine(outputProperties, "AssemblyInfo.cs"), projectVersion); - GenerateSolution(quantities, outputDir); - - var unitCount = quantities.SelectMany(q => q.Units).Count(); - Log.Information(""); - Log.Information("Total of {UnitCount} units and {QuantityCount} quantities (nanoFramework)", unitCount, quantities.Length); - Log.Information(""); - } - - /// - /// Updates existing nanoFramework projects and nuspec files with the latest versions. - /// - /// The root directory - /// The quantities to update nuspec files - public static bool UpdateNanoFrameworkDependencies( - string rootDir, - Quantity[] quantities) - { - // working path - var path = Path.Combine(rootDir, "UnitsNet.NanoFramework\\GeneratedCode"); - - Log.Information(""); - Log.Information("Restoring .NET nanoFramework projects"); - - // run nuget CLI - using var nugetRestore = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = Path.Combine(rootDir, ".tools/nuget.exe"), - Arguments = $"restore {path}\\UnitsNet.nanoFramework.sln", - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true - } - }; - - // start nuget CLI and wait for exit - if (!nugetRestore.Start()) - { - Log.Information(""); - Log.Information("Failed to start nuget CLI to restore .NET nanoFramework projects"); - Log.Information(""); - } - else - { - // wait for exit, within 2 minutes - if (!nugetRestore.WaitForExit((int)TimeSpan.FromMinutes(2).TotalMilliseconds)) - { - Log.Information(""); - Log.Information("Failed to complete execution of nuget CLI to restore .NET nanoFramework projects"); - Log.Information(""); - } - else - { - if (nugetRestore.ExitCode == 0) - { - Log.Information("Done!"); - Log.Information(""); - } - else - { - Log.Information(""); - Log.Information("nuget CLI executed with {ExitCode} exit code", nugetRestore.ExitCode); - - Log.Information("{StandardError}", nugetRestore.StandardError.ReadToEnd()); - - return false; - } - } - } - - Log.Information(""); - Log.Information("Updating .NET nanoFramework references using nuget CLI"); - - // run nuget CLI to perform update - using var nugetUpdate = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = Path.Combine(rootDir, ".tools/NuGet.exe"), - Arguments = $"update {path}\\UnitsNet.nanoFramework.sln -PreRelease", - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true - } - }; - - // start nuget CLI and wait for exit - if (!nugetUpdate.Start()) - { - Log.Information(""); - Log.Information("Failed to start nuget CLI to update .NET nanoFramework projects"); - Log.Information(""); - } - else - { - // wait for exit, within 2 minutes - if (!nugetUpdate.WaitForExit((int)TimeSpan.FromMinutes(2).TotalMilliseconds)) - { - Log.Information(""); - Log.Information("Failed to complete execution of nuget CLI to update .NET nanoFramework projects"); - Log.Information(""); - } - else - { - if (nugetUpdate.ExitCode == 0) - { - Log.Information("Done!"); - Log.Information(""); - - Log.Information("Updating .NET nanoFramework nuspec files"); - Log.Information(""); - - foreach (Quantity quantity in quantities) - { - var projectPath = Path.Combine(path, quantity.Name); - - // read packages.config content - var packagesConfig = Path.Combine(projectPath, "packages.config"); - var packagesConfigText = File.ReadAllText(packagesConfig); - - var mscorlibVersion = ParseVersion(packagesConfigText, - new Regex("CoreLibrary\\\" version=\\\"(?.+)\\\" targetFramework", RegexOptions.IgnoreCase), - "projectVersion"); - - // don't throw on failure because not all packages have System.Math - var mathVersion = ParseVersion(packagesConfigText, - new Regex("Math\\\" version=\\\"(?.+)\\\" targetFramework", RegexOptions.IgnoreCase), - "projectVersion", - false); - - // update nuspec - GenerateNuspec( - projectPath, - quantity, - mscorlibVersion, - mathVersion); - - Log.Information("✅ {Quantity} (nanoFramework)", quantity.Name); - } - } - else - { - Log.Information(""); - Log.Information("nuget CLI executed with {ExitCode} exit code", nugetUpdate.ExitCode); - - Log.Information("{StandardError}", nugetUpdate.StandardError.ReadToEnd()); - - return false; - } - } - - } - - Log.Information(""); - - return true; - } - - private static NanoFrameworkVersions ParseCurrentNanoFrameworkVersions(string rootDir) - { - // Angle has both mscorlib and System.Math dependency - var generatedCodePath = Path.Combine(rootDir, "UnitsNet.NanoFramework", "GeneratedCode"); - var angleProjectFile = Path.Combine(generatedCodePath, "Angle", "Angle.nfproj"); - var projectFileContent = File.ReadAllText(angleProjectFile); - - // - var mscorlibVersion = ParseVersion(projectFileContent, - new Regex(@"[\d\.]+),.*"">", RegexOptions.IgnoreCase), - "mscorlib assembly version"); - - // ..\packages\nanoFramework.CoreLibrary.1.10.5-preview.18\lib\mscorlib.dll - var mscorlibNuGetVersion = ParseVersion(projectFileContent, - new Regex(@".*[\\\/]nanoFramework\.CoreLibrary\.(?.*?)[\\\/]lib[\\\/]mscorlib.dll<", RegexOptions.IgnoreCase), - "nanoFramework.CoreLibrary nuget version"); - - // - var mathVersion = ParseVersion(projectFileContent, - new Regex(@"[\d\.]+),.*"">", RegexOptions.IgnoreCase), - "System.Math assembly version"); - - // ..\packages\nanoFramework.System.Math.1.4.1-preview.7\lib\System.Math.dll - var mathNuGetVersion = ParseVersion(projectFileContent, - new Regex(@".*[\\\/]nanoFramework\.System\.Math\.(?.*?)[\\\/]lib[\\\/]System.Math.dll<", RegexOptions.IgnoreCase), - "nanoFramework.System.Math nuget version"); - - return new NanoFrameworkVersions(mscorlibVersion, mscorlibNuGetVersion, mathVersion, mathNuGetVersion); - } - - private static string ParseVersion( - string projectFileContent, - Regex versionRegex, - string descriptiveName, - bool throwOnFailure = true) - { - Match match = versionRegex.Match(projectFileContent); - - if (!match.Success && throwOnFailure) - { - throw new InvalidOperationException($"Unable to parse version {descriptiveName} from project file."); - } - - return match.Groups["version"].Value; - } - - private static void GeneratePackageConfig( - string projectPath, - string quantityName, - string mscorlibNuGetVersion, - string mathNuGetVersion) - { - var filePath = Path.Combine(projectPath, "packages.config"); - var content = GeneratePackageConfigFile(quantityName, mscorlibNuGetVersion, mathNuGetVersion); - - File.WriteAllText(filePath, content); - } - - private static void GenerateNuspec( - string projectPath, - Quantity quantity, - string mscorlibNuGetVersion, - string mathNuGetVersion) - { - var filePath = Path.Combine(projectPath, $"UnitsNet.NanoFramework.{quantity.Name}.nuspec"); - - var content = new NuspecGenerator( - quantity, - mscorlibNuGetVersion, - mathNuGetVersion).Generate(); - - File.WriteAllText(filePath, content); - } - - private static void GenerateProperties(string filePath, string version) - { - var content = new PropertyGenerator(version).Generate(); - File.WriteAllText(filePath, content); - Log.Information("✅ AssemblyInfo.cs (nanoFramework)"); - } - - private static void GenerateUnitType(Quantity quantity, string filePath, UnitEnumNameToValue unitEnumValues) - { - var content = new UnitTypeGenerator(quantity, unitEnumValues).Generate(); - File.WriteAllText(filePath, content); - } - - private static void GenerateQuantity(Quantity quantity, string filePath) - { - var content = new QuantityGenerator(quantity).Generate(); - // Replace any Math.PI by the real number 3.1415926535897931 - content = content.Replace("Math.PI", "3.1415926535897931"); - // Replace Math.Pow(0.3048, 4) by 0.0086309748412416 - content = content.Replace("Math.Pow(0.3048, 4)", "0.0086309748412416"); - // Replace Math.Pow(2.54e-2, 4) by 0.0000004162314256 - content = content.Replace("Math.Pow(2.54e-2, 4)", "0.0000004162314256"); - File.WriteAllText(filePath, content); - } - - private static void GenerateProject(Quantity quantity, string filePath, NanoFrameworkVersions versions) - { - var content = new ProjectGenerator(quantity, versions).Generate(); - File.WriteAllText(filePath, content); - } - - private static void GenerateSolution(Quantity[] quantities, string outputDir) - { - var content = new SolutionGenerator(quantities).Generate(); - var filePath = Path.Combine(outputDir, "UnitsNet.nanoFramework.sln"); - - File.WriteAllText(filePath, content); - Log.Information("✅ UnitsNet.nanoFramework.sln (nanoFramework)"); - } - - private static string GeneratePackageConfigFile( - string quantityName, - string mscorlibNuGetVersion, - string mathNuGetVersion) - { - MyTextWriter writer = new(); - - writer.WL($@" - - - "); - - if (ProjectsRequiringMath.Contains(quantityName)) - { - writer.WL($@" - "); - } - - writer.WL($@""); - - return writer.ToString(); - } - } -} diff --git a/CodeGen/Generators/NanoFrameworkVersions.cs b/CodeGen/Generators/NanoFrameworkVersions.cs deleted file mode 100644 index c9da440cf1..0000000000 --- a/CodeGen/Generators/NanoFrameworkVersions.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. -// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. - -namespace CodeGen.Generators -{ - /// - /// NanoFramework dependency versions. - /// - /// mscorlib assembly version in nanoFramework.CoreLibrary nuget. - /// Nuget version of nanoFramework.CoreLibrary. - /// System.Math assembly version in nanoFramework.System.Math nuget. - /// Nuget version of nanoFramework.System.Math. - public record NanoFrameworkVersions(string MscorlibVersion, string MscorlibNugetVersion, string MathVersion, string MathNugetVersion); -} diff --git a/CodeGen/Generators/QuantityJsonFilesParser.cs b/CodeGen/Generators/QuantityJsonFilesParser.cs index e1d50871a7..d6c503fd5c 100644 --- a/CodeGen/Generators/QuantityJsonFilesParser.cs +++ b/CodeGen/Generators/QuantityJsonFilesParser.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using CodeGen.Exceptions; +using CodeGen.Helpers; using CodeGen.Helpers.PrefixBuilder; using CodeGen.JsonTypes; using Newtonsoft.Json; @@ -16,7 +17,7 @@ namespace CodeGen.Generators; /// /// Parses JSON files that define quantities and their units. /// This will later be used to generate source code and can be reused for different targets such as .NET framework, -/// .NET Core, .NET nanoFramework and even other programming languages. +/// .NET Core and even other programming languages. /// internal static class QuantityJsonFilesParser { @@ -54,7 +55,7 @@ private static Quantity ParseQuantity(string jsonFileName) { try { - return JsonConvert.DeserializeObject(File.ReadAllText(jsonFileName), JsonSerializerSettings) + return JsonConvert.DeserializeObject(CodeGenFile.ReadAllText(jsonFileName), JsonSerializerSettings) ?? throw new UnitsNetCodeGenException($"Unable to parse quantity from JSON file: {jsonFileName}"); } catch (Exception e) diff --git a/CodeGen/Generators/QuantityRelationsParser.cs b/CodeGen/Generators/QuantityRelationsParser.cs index 401c7e7b10..658964e2c1 100644 --- a/CodeGen/Generators/QuantityRelationsParser.cs +++ b/CodeGen/Generators/QuantityRelationsParser.cs @@ -1,11 +1,13 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using CodeGen.Exceptions; +using CodeGen.Helpers; using CodeGen.JsonTypes; using Newtonsoft.Json; @@ -24,7 +26,7 @@ internal static class QuantityRelationsParser /// Each defined relation can be applied multiple times to one or two quantities depending on the operator and the operands. /// /// The format of a relation definition is "Quantity.Unit operator Quantity.Unit = Quantity.Unit" (See examples below). - /// "double" can be used as a unitless operand. + /// "QuantityValue" can be used as a unitless operand. /// "1" can be used as the result operand to define inverse relations. /// /// Division relations are inferred from multiplication relations, @@ -43,9 +45,9 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities) { var quantityDictionary = quantities.ToDictionary(q => q.Name, q => q); - // Add double and 1 as pseudo-quantities to validate relations that use them. + // Add QuantityValue and 1 as pseudo-quantities to validate relations that use them. var pseudoQuantity = new Quantity { Name = null!, Units = [new Unit { SingularName = null! }] }; - quantityDictionary["double"] = pseudoQuantity with { Name = "double" }; + quantityDictionary["QuantityValue"] = pseudoQuantity with { Name = "QuantityValue" }; quantityDictionary["1"] = pseudoQuantity with { Name = "1" }; var relations = ParseRelations(rootDir, quantityDictionary); @@ -61,7 +63,7 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities) RightUnit = r.LeftUnit, }) .ToList()); - + // We can infer division relations from multiplication relations. relations.AddRange(relations .Where(r => r is { Operator: "*", NoInferredDivision: false }) @@ -91,7 +93,7 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities) var list = string.Join("\n ", duplicates); throw new UnitsNetCodeGenException($"Duplicate inferred relations:\n {list}"); } - + var ambiguous = relations .GroupBy(r => $"{r.LeftQuantity.Name} {r.Operator} {r.RightQuantity.Name}") .Where(g => g.Count() > 1) @@ -115,9 +117,9 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities) // The left operand of a relation is responsible for generating the operator. quantityRelations.Add(relation); } - else if (relation.RightQuantity == quantity && relation.LeftQuantity.Name is "double") + else if (relation.RightQuantity == quantity && relation.LeftQuantity.Name is "QuantityValue") { - // Because we cannot add operators to double we make the right operand responsible in this case. + // Because we cannot add operators to QuantityValue we make the right operand responsible in this case. quantityRelations.Add(relation); } } @@ -132,13 +134,16 @@ private static List ParseRelations(string rootDir, IReadOnlyDi try { - var text = File.ReadAllText(relationsFileName); - var relationStrings = JsonConvert.DeserializeObject>(text) ?? []; + var text = CodeGenFile.ReadAllText(relationsFileName); + + // Explicitly sort to keep the file consistent. + var relationStrings = JsonConvert.DeserializeObject>(text) + ?.ToImmutableSortedSet(StringComparer.OrdinalIgnoreCase) ?? []; var parsedRelations = relationStrings.Select(relationString => ParseRelation(relationString, quantities)).ToList(); // File parsed successfully, save it back to disk in the sorted state. - File.WriteAllText(relationsFileName, JsonConvert.SerializeObject(relationStrings, Formatting.Indented)); + CodeGenFile.WriteAllText(relationsFileName, JsonConvert.SerializeObject(relationStrings, Formatting.Indented)); return parsedRelations; } @@ -210,4 +215,4 @@ Unit GetUnit(Quantity quantity, string? unitName) } } } -} \ No newline at end of file +} diff --git a/CodeGen/Generators/UnitsNetGen/IQuantityTestClassGenerator.cs b/CodeGen/Generators/UnitsNetGen/IQuantityTestClassGenerator.cs index 43616e03f6..2786525dcb 100644 --- a/CodeGen/Generators/UnitsNetGen/IQuantityTestClassGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/IQuantityTestClassGenerator.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System.Diagnostics.CodeAnalysis; diff --git a/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14Generator.cs b/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14Generator.cs index f815158c3a..b7ac802211 100644 --- a/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14Generator.cs +++ b/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14Generator.cs @@ -1,4 +1,4 @@ -using System; +using System; using CodeGen.JsonTypes; namespace CodeGen.Generators.UnitsNetGen @@ -53,16 +53,16 @@ public static class NumberTo{_quantityName}Extensions continue; Writer.WL(3, $@" -/// "); +/// "); // Include obsolete text from the quantity per extension method, to make it visible when the class is not explicitly referenced in code. Writer.WLIfText(3, GetObsoleteAttributeOrNull(unit.ObsoleteText ?? _quantity.ObsoleteText)); Writer.WL(3, $@"public {_quantityName} {unit.PluralName} #if NET7_0_OR_GREATER - => {_quantityName}.From{unit.PluralName}(double.CreateChecked(value)); + => {_quantityName}.From{unit.PluralName}(QuantityValue.CreateChecked(value)); #else - => {_quantityName}.From{unit.PluralName}(value.ToDouble(null)); + => {_quantityName}.From{unit.PluralName}(value.ToQuantityValue()); #endif "); } diff --git a/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14TestClassGenerator.cs b/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14TestClassGenerator.cs index d3573fcaf5..ead5a22148 100644 --- a/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14TestClassGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/NumberExtensionsCS14TestClassGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using CodeGen.JsonTypes; namespace CodeGen.Generators.UnitsNetGen diff --git a/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs b/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs index 4765e49d9e..74a57acc1e 100644 --- a/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/NumberExtensionsGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using CodeGen.JsonTypes; namespace CodeGen.Generators.UnitsNetGen @@ -40,7 +40,7 @@ public static class NumberTo{_quantityName}Extensions continue; Writer.WL(2, $@" -/// "); +/// "); // Include obsolete text from the quantity per extension method, to make it visible when the class is not explicitly referenced in code. Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit.ObsoleteText ?? _quantity.ObsoleteText)); @@ -49,10 +49,10 @@ public static class NumberTo{_quantityName}Extensions where T : notnull #if NET7_0_OR_GREATER , INumber - => {_quantityName}.From{unit.PluralName}(double.CreateChecked(value)); + => {_quantityName}.From{unit.PluralName}(QuantityValue.CreateChecked(value)); #else , IConvertible - => {_quantityName}.From{unit.PluralName}(value.ToDouble(null)); + => {_quantityName}.From{unit.PluralName}(value.ToQuantityValue()); #endif "); } diff --git a/CodeGen/Generators/UnitsNetGen/NumberExtensionsTestClassGenerator.cs b/CodeGen/Generators/UnitsNetGen/NumberExtensionsTestClassGenerator.cs index b5cced30fd..6d64a410fc 100644 --- a/CodeGen/Generators/UnitsNetGen/NumberExtensionsTestClassGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/NumberExtensionsTestClassGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using CodeGen.JsonTypes; namespace CodeGen.Generators.UnitsNetGen diff --git a/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs b/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs index dfaa53af3d..985a8f5ebb 100644 --- a/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs @@ -1,10 +1,13 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; using System.Linq; using CodeGen.Helpers; +using CodeGen.Helpers.ExpressionAnalyzer; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; using CodeGen.JsonTypes; +using Fractions; namespace CodeGen.Generators.UnitsNetGen { @@ -34,13 +37,10 @@ public string Generate() { Writer.WL(GeneratedFileHeader); Writer.WL(@" - using System.Globalization; using System.Resources; using System.Runtime.Serialization; -#if NET -using System.Numerics; -#endif +using UnitsNet.Debug; #nullable enable @@ -62,7 +62,8 @@ namespace UnitsNet Writer.WLIfText(1, GetObsoleteAttributeOrNull(_quantity)); Writer.WL(@$" [DataContract] - [DebuggerTypeProxy(typeof(QuantityDisplay))] + [DebuggerDisplay(QuantityDebugProxy.DisplayFormat)] + [DebuggerTypeProxy(typeof(QuantityDebugProxy))] public readonly partial struct {_quantity.Name} :"); GenerateInterfaceExtensions(); @@ -72,7 +73,7 @@ namespace UnitsNet /// The numeric value this quantity was constructed with. /// [DataMember(Name = ""Value"", Order = 1)] - private readonly double _value; + private readonly QuantityValue _value; /// /// The unit this quantity was constructed with. @@ -92,7 +93,6 @@ namespace UnitsNet GenerateArithmeticOperators(); GenerateRelationalOperators(); GenerateEqualityAndComparison(); - GenerateConversionMethods(); GenerateToString(); Writer.WL($@" @@ -103,21 +103,21 @@ namespace UnitsNet private void GenerateInterfaceExtensions() { - // generate the base interface (either IVectorQuantity, IAffineQuantity or ILogarithmicQuantity) + // generate ILogarithmicQuantity, IAffineQuantity or ILinearQuantity if (_quantity.Logarithmic) { - Writer.WL(@$" + Writer.WL($@" ILogarithmicQuantity<{_quantity.Name}, {_unitEnumName}>,"); } else if (!string.IsNullOrEmpty(_quantity.AffineOffsetType)) { - Writer.WL(@$" + Writer.WL($@" IAffineQuantity<{_quantity.Name}, {_unitEnumName}, {_quantity.AffineOffsetType}>,"); } - else // the default quantity type implements the IVectorQuantity interface + else // the default quantity type implements the ILinearQuantity interface { - Writer.WL(@$" - IArithmeticQuantity<{_quantity.Name}, {_unitEnumName}>,"); + Writer.WL($@" + ILinearQuantity<{_quantity.Name}, {_unitEnumName}>,"); } Writer.WL(@" @@ -125,7 +125,7 @@ private void GenerateInterfaceExtensions() if (!_quantity.IsAffine) { Writer.WL($@" - IDivisionOperators<{_quantity.Name}, {_quantity.Name}, double>,"); + IDivisionOperators<{_quantity.Name}, {_quantity.Name}, QuantityValue>,"); } if (_quantity.Relations.Any(r => r.Operator is "*" or "/")) @@ -252,8 +252,35 @@ public sealed class {quantityInfoClassName}: QuantityInfo<{_quantity.Name}, {_un }.Where(str => str != null))})"; } - Writer.WL($@" + // the UnitInfo constructor has 3 overloads: + // - one for the base unit without conversion expressions + // - one for units with only FromBaseToUnit conversion expression (with the FromUnitToBase expression assumed to be the inverse) + // - one for units with both FromBaseToUnit and FromUnitToBase conversion expressions (required when the conversion is not a simple inverse, e.g. affine conversions) + if (unit.SingularName == _quantity.BaseUnit) + { + Writer.WL($@" yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat});"); + } + else + { + CompositeExpression expressionFromBaseToUnit = ExpressionEvaluator.Evaluate(unit.FromBaseToUnitFunc, "{x}"); + // Check if FromUnitToBase is simply the inverse of FromBaseToUnit + if (expressionFromBaseToUnit.Terms.Count == 1 && expressionFromBaseToUnit.Degree == Fraction.One) + { + Writer.WL($@" + yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat}, + {expressionFromBaseToUnit.GetConversionExpressionFormat()} + );"); + } + else + { + Writer.WL($@" + yield return new ({_unitEnumName}.{unit.SingularName}, ""{unit.SingularName}"", ""{unit.PluralName}"", {baseUnitsFormat}, + {expressionFromBaseToUnit.GetConversionExpressionFormat()}, + {unit.GetUnitToBaseConversionExpressionFormat()} + );"); + } + } } Writer.WL($@" @@ -268,9 +295,7 @@ private void GenerateStaticConstructor() static {_quantity.Name}() {{"); Writer.WL($@" - Info = {_quantity.Name}Info.CreateDefault(); - DefaultConversionFunctions = new UnitConverter(); - RegisterDefaultConversions(DefaultConversionFunctions); + Info = UnitsNetSetup.CreateQuantityInfo({_quantity.Name}Info.CreateDefault); }} "); } @@ -283,7 +308,7 @@ private void GenerateInstanceConstructors() /// /// The numeric value to construct this quantity with. /// The unit representation to construct this quantity with. - public {_quantity.Name}(double value, {_unitEnumName} unit) + public {_quantity.Name}(QuantityValue value, {_unitEnumName} unit) {{"); Writer.WL(@" _value = value;"); @@ -302,7 +327,7 @@ private void GenerateInstanceConstructors() /// The unit system to create the quantity with. /// The given is null. /// No unit was found for the given . - public {_quantity.Name}(double value, UnitSystem unitSystem) + public {_quantity.Name}(QuantityValue value, UnitSystem unitSystem) {{ _value = value; _unit = Info.GetDefaultUnit(unitSystem); @@ -319,7 +344,8 @@ private void GenerateStaticProperties() /// /// The containing the default generated conversion functions for instances. /// - public static UnitConverter DefaultConversionFunctions {{ get; }} + [Obsolete(""Replaced by UnitConverter.Default"")] + public static UnitConverter DefaultConversionFunctions => UnitConverter.Default; /// public static QuantityInfo<{_quantity.Name}, {_unitEnumName}> Info {{ get; }} @@ -349,7 +375,7 @@ private void GenerateStaticProperties() { Writer.WL($@" /// - public static double LogarithmicScalingFactor {{get;}} = {10 * _quantity.LogarithmicScalingFactor}; + public static QuantityValue LogarithmicScalingFactor {{get;}} = {10 * _quantity.LogarithmicScalingFactor}; "); } @@ -363,10 +389,8 @@ private void GenerateProperties() Writer.WL($@" #region Properties - /// - /// The numeric value this quantity was constructed with. - /// - public double Value => _value; + /// + public QuantityValue Value => _value; /// public {_unitEnumName} Unit => _unit.GetValueOrDefault(BaseUnit); @@ -397,7 +421,7 @@ private void GenerateProperties() { Writer.WL($@" #if NETSTANDARD2_0 - double ILogarithmicQuantity<{_quantity.Name}>.LogarithmicScalingFactor => LogarithmicScalingFactor; + QuantityValue ILogarithmicQuantity<{_quantity.Name}>.LogarithmicScalingFactor => LogarithmicScalingFactor; #endif "); } @@ -420,11 +444,11 @@ private void GenerateConversionProperties() Writer.WL($@" /// - /// Gets a value of this quantity converted into + /// Gets a value of this quantity converted into /// "); Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit)); Writer.WL($@" - public double {unit.PluralName} => As({_unitEnumName}.{unit.SingularName}); + public QuantityValue {unit.PluralName} => this.As({_unitEnumName}.{unit.SingularName}); "); } @@ -440,41 +464,6 @@ private void GenerateStaticMethods() #region Static Methods - /// - /// Registers the default conversion functions in the given instance. - /// - /// The to register the default conversion functions in. - internal static void RegisterDefaultConversions(UnitConverter unitConverter) - {{ - // Register in unit converter: {_unitEnumName} -> BaseUnit"); - - foreach (Unit unit in _quantity.Units) - { - if (unit.SingularName == _quantity.BaseUnit) continue; - - Writer.WL($@" - unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity.ToUnit({_unitEnumName}.{_quantity.BaseUnit}));"); - } - - Writer.WL(); - Writer.WL($@" - - // Register in unit converter: BaseUnit <-> BaseUnit - unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{_quantity.BaseUnit}, quantity => quantity); - - // Register in unit converter: BaseUnit -> {_unitEnumName}"); - - foreach (Unit unit in _quantity.Units) - { - if (unit.SingularName == _quantity.BaseUnit) continue; - - Writer.WL($@" - unitConverter.SetConversionFunction<{_quantity.Name}>({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}, quantity => quantity.ToUnit({_unitEnumName}.{unit.SingularName}));"); - } - - Writer.WL($@" - }} - /// /// Get unit abbreviation string. /// @@ -515,7 +504,7 @@ private void GenerateStaticFactoryMethods() /// "); Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit)); Writer.WL($@" - public static {_quantity.Name} From{unit.PluralName}(double value) + public static {_quantity.Name} From{unit.PluralName}(QuantityValue value) {{ return new {_quantity.Name}(value, {_unitEnumName}.{unit.SingularName}); }} @@ -529,7 +518,7 @@ private void GenerateStaticFactoryMethods() /// Value to convert from. /// Unit to convert from. /// {_quantity.Name} unit value. - public static {_quantity.Name} From(double value, {_unitEnumName} fromUnit) + public static {_quantity.Name} From(QuantityValue value, {_unitEnumName} fromUnit) {{ return new {_quantity.Name}(value, fromUnit); }} @@ -595,10 +584,7 @@ private void GenerateStaticParseMethods() /// Format to use when parsing number and unit. Defaults to if null. public static {_quantity.Name} Parse(string str, IFormatProvider? provider) {{ - return UnitsNetSetup.Default.QuantityParser.Parse<{_quantity.Name}, {_unitEnumName}>( - str, - provider, - From); + return QuantityParser.Default.Parse<{_quantity.Name}, {_unitEnumName}>(str, provider, From); }} /// @@ -626,11 +612,7 @@ public static bool TryParse([NotNullWhen(true)]string? str, out {_quantity.Name} /// Format to use when parsing number and unit. Defaults to if null. public static bool TryParse([NotNullWhen(true)]string? str, IFormatProvider? provider, out {_quantity.Name} result) {{ - return UnitsNetSetup.Default.QuantityParser.TryParse<{_quantity.Name}, {_unitEnumName}>( - str, - provider, - From, - out result); + return QuantityParser.Default.TryParse<{_quantity.Name}, {_unitEnumName}>(str, provider, From, out result); }} /// @@ -651,7 +633,7 @@ public static bool TryParse([NotNullWhen(true)]string? str, IFormatProvider? pro /// Parse a unit string. /// /// String to parse. Typically in the form: {{number}} {{unit}} - /// Format to use when parsing number and unit. Defaults to if null. + /// Format to use when parsing the unit. Defaults to if null. /// /// Length.ParseUnit(""m"", CultureInfo.GetCultureInfo(""en-US"")); /// @@ -662,7 +644,7 @@ public static bool TryParse([NotNullWhen(true)]string? str, IFormatProvider? pro return UnitParser.Default.Parse(str, Info.UnitInfos, provider).Value; }} - /// + /// public static bool TryParseUnit([NotNullWhen(true)]string? str, out {_unitEnumName} unit) {{ return TryParseUnit(str, null, out unit); @@ -677,7 +659,7 @@ public static bool TryParseUnit([NotNullWhen(true)]string? str, out {_unitEnumNa /// /// Length.TryParseUnit(""m"", CultureInfo.GetCultureInfo(""en-US"")); /// - /// Format to use when parsing number and unit. Defaults to if null. + /// Format to use when parsing the unit. Defaults to if null. public static bool TryParseUnit([NotNullWhen(true)]string? str, IFormatProvider? provider, out {_unitEnumName} unit) {{ return UnitParser.Default.TryParse(str, Info, provider, out unit); @@ -715,35 +697,35 @@ private void GenerateArithmeticOperators() /// Get from adding two . public static {_quantity.Name} operator +({_quantity.Name} left, {_quantity.Name} right) {{ - return new {_quantity.Name}(left.Value + right.ToUnit(left.Unit).Value, left.Unit); + return new {_quantity.Name}(left.Value + right.As(left.Unit), left.Unit); }} /// Get from subtracting two . public static {_quantity.Name} operator -({_quantity.Name} left, {_quantity.Name} right) {{ - return new {_quantity.Name}(left.Value - right.ToUnit(left.Unit).Value, left.Unit); + return new {_quantity.Name}(left.Value - right.As(left.Unit), left.Unit); }} /// Get from multiplying value and . - public static {_quantity.Name} operator *(double left, {_quantity.Name} right) + public static {_quantity.Name} operator *(QuantityValue left, {_quantity.Name} right) {{ return new {_quantity.Name}(left * right.Value, right.Unit); }} /// Get from multiplying value and . - public static {_quantity.Name} operator *({_quantity.Name} left, double right) + public static {_quantity.Name} operator *({_quantity.Name} left, QuantityValue right) {{ return new {_quantity.Name}(left.Value * right, left.Unit); }} /// Get from dividing by value. - public static {_quantity.Name} operator /({_quantity.Name} left, double right) + public static {_quantity.Name} operator /({_quantity.Name} left, QuantityValue right) {{ return new {_quantity.Name}(left.Value / right, left.Unit); }} /// Get ratio value from dividing by . - public static double operator /({_quantity.Name} left, {_quantity.Name} right) + public static QuantityValue operator /({_quantity.Name} left, {_quantity.Name} right) {{ return left.{_baseUnit.PluralName} / right.{_baseUnit.PluralName}; }} @@ -754,60 +736,56 @@ private void GenerateArithmeticOperators() private void GenerateLogarithmicArithmeticOperators() { - var scalingFactor = _quantity.LogarithmicScalingFactor; // Most logarithmic operators need a simple scaling factor of 10. However, certain units such as voltage ratio need to use 20 instead. - var x = (10 * scalingFactor).ToString(); Writer.WL($@" #region Logarithmic Arithmetic Operators /// Negate the value. - public static {_quantity.Name} operator -({_quantity.Name} right) + public static {_quantity.Name} operator -({_quantity.Name} quantity) {{ - return new {_quantity.Name}(-right.Value, right.Unit); + return new {_quantity.Name}(-quantity.Value, quantity.Unit); }} /// Get from logarithmic addition of two . + /// This operation involves a conversion of the values to linear space, which is not guaranteed to produce an exact value. + /// The final result is rounded to 15 significant digits. + /// public static {_quantity.Name} operator +({_quantity.Name} left, {_quantity.Name} right) {{ - // Logarithmic addition - // Formula: {x} * log10(10^(x/{x}) + 10^(y/{x})) - return new {_quantity.Name}({x} * Math.Log10(Math.Pow(10, left.Value / {x}) + Math.Pow(10, right.ToUnit(left.Unit).Value / {x})), left.Unit); + return new {_quantity.Name}(QuantityValueExtensions.AddWithLogScaling(left.Value, right.As(left.Unit), LogarithmicScalingFactor), left.Unit); }} /// Get from logarithmic subtraction of two . + /// This operation involves a conversion of the values to linear space, which is not guaranteed to produce an exact value. + /// The final result is rounded to 15 significant digits. + /// public static {_quantity.Name} operator -({_quantity.Name} left, {_quantity.Name} right) {{ - // Logarithmic subtraction - // Formula: {x} * log10(10^(x/{x}) - 10^(y/{x})) - return new {_quantity.Name}({x} * Math.Log10(Math.Pow(10, left.Value / {x}) - Math.Pow(10, right.ToUnit(left.Unit).Value / {x})), left.Unit); + return new {_quantity.Name}(QuantityValueExtensions.SubtractWithLogScaling(left.Value, right.As(left.Unit), LogarithmicScalingFactor), left.Unit); }} /// Get from logarithmic multiplication of value and . - public static {_quantity.Name} operator *(double left, {_quantity.Name} right) + public static {_quantity.Name} operator *(QuantityValue left, {_quantity.Name} right) {{ - // Logarithmic multiplication = addition return new {_quantity.Name}(left + right.Value, right.Unit); }} /// Get from logarithmic multiplication of value and . - public static {_quantity.Name} operator *({_quantity.Name} left, double right) + public static {_quantity.Name} operator *({_quantity.Name} left, QuantityValue right) {{ - // Logarithmic multiplication = addition return new {_quantity.Name}(left.Value + right, left.Unit); }} /// Get from logarithmic division of by value. - public static {_quantity.Name} operator /({_quantity.Name} left, double right) + public static {_quantity.Name} operator /({_quantity.Name} left, QuantityValue right) {{ - // Logarithmic division = subtraction return new {_quantity.Name}(left.Value - right, left.Unit); }} /// Get ratio value from logarithmic division of by . - public static double operator /({_quantity.Name} left, {_quantity.Name} right) + public static QuantityValue operator /({_quantity.Name} left, {_quantity.Name} right) {{ - // Logarithmic division = subtraction - return Convert.ToDouble(left.Value - right.ToUnit(left.Unit).Value); + return left.Value - right.As(left.Unit); }} #endregion @@ -815,11 +793,16 @@ private void GenerateLogarithmicArithmeticOperators() } /// - /// Generates operators that express relations between quantities as applied by . + /// Generates relational operators for quantities based on their defined relations. /// - private void GenerateRelationalOperators() + /// + /// Specifies whether inverse relational operators should be generated as implicit conversions or simply using the unit specified in the UnitRelations. + /// If true, the method generates inverse operators that convert to a fixed unit, as specified in the UnitRelations. + /// If false, the method generates inverse operators as implicit conversions that utilize the UnitConverter for conversion. + /// + private void GenerateRelationalOperators(bool inverseWithFixedUnit = false) { - if (!_quantity.Relations.Any()) return; + if (_quantity.Relations.Length == 0) return; Writer.WL($@" #region Relational Operators @@ -829,51 +812,64 @@ private void GenerateRelationalOperators() { if (relation.Operator == "inverse") { - Writer.WL($@" + if (inverseWithFixedUnit) + { + // this was the original behavior where the inverse always used the fixed unit from the relation + Writer.WL($@" /// Calculates the inverse of this quantity. /// The corresponding inverse quantity, . public {relation.RightQuantity.Name} Inverse() {{ - return {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(1 / {relation.LeftUnit.PluralName}); + return {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(QuantityValue.Inverse({relation.LeftUnit.PluralName})); }} "); + } + else + { + // this is the proposed improvement where the inverse is considered a type of implicit conversion + Writer.WL($@" + /// Calculates the inverse of this quantity. + /// The corresponding inverse quantity, . + public {relation.RightQuantity.Name} Inverse() + {{ + return UnitConverter.Default.ConvertTo(Value, Unit, {relation.RightQuantity.Name}.Info); + }} +"); + } } else { - var leftParameter = relation.LeftQuantity.Name.ToCamelCase(); + const string valueType = "QuantityValue"; + var leftParameterType = relation.LeftQuantity.Name; var leftConversionProperty = relation.LeftUnit.PluralName; - var rightParameter = relation.RightQuantity.Name.ToCamelCase(); + var rightParameterType = relation.RightQuantity.Name; var rightConversionProperty = relation.RightUnit.PluralName; - if (leftParameter == rightParameter) + string leftParameterName, rightParameterName; + if (leftParameterType == rightParameterType) { - leftParameter = "left"; - rightParameter = "right"; + leftParameterName = "left"; + rightParameterName = "right"; } - - var leftPart = $"{leftParameter}.{leftConversionProperty}"; - var rightPart = $"{rightParameter}.{rightConversionProperty}"; - - if (leftParameter is "double") + else { - leftParameter = leftPart = "value"; - } - - if (rightParameter is "double") - { - rightParameter = rightPart = "value"; + leftParameterName = leftParameterType is valueType ? "value": leftParameterType.ToCamelCase(); + rightParameterName = rightParameterType is valueType ? "value": rightParameterType.ToCamelCase(); } + var leftPart = leftParameterType is valueType ? leftParameterName : $"{leftParameterName}.{leftConversionProperty}"; + var rightPart = rightParameterName is valueType ? rightParameterName : $"{rightParameterName}.{rightConversionProperty}"; var expression = $"{leftPart} {relation.Operator} {rightPart}"; - if (relation.ResultQuantity.Name is not "double") + var resultType = relation.ResultQuantity.Name; + if (resultType is not valueType) { - expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})"; + expression = $"{resultType}.From{relation.ResultUnit.PluralName}({expression})"; } Writer.WL($@" - /// Get from {relation.Operator} . - public static {relation.ResultQuantity.Name} operator {relation.Operator}({relation.LeftQuantity.Name} {leftParameter}, {relation.RightQuantity.Name} {rightParameter}) + /// Get from {relation.Operator} . + public static {resultType} operator {relation.Operator}({leftParameterType} {leftParameterName}, {rightParameterType} {rightParameterName}) {{ return {expression}; }} @@ -895,97 +891,111 @@ private void GenerateEqualityAndComparison() /// Returns true if less or equal to. public static bool operator <=({_quantity.Name} left, {_quantity.Name} right) {{ - return left.Value <= right.ToUnit(left.Unit).Value; + return left.Value <= right.As(left.Unit); }} /// Returns true if greater than or equal to. public static bool operator >=({_quantity.Name} left, {_quantity.Name} right) {{ - return left.Value >= right.ToUnit(left.Unit).Value; + return left.Value >= right.As(left.Unit); }} /// Returns true if less than. public static bool operator <({_quantity.Name} left, {_quantity.Name} right) {{ - return left.Value < right.ToUnit(left.Unit).Value; + return left.Value < right.As(left.Unit); }} /// Returns true if greater than. public static bool operator >({_quantity.Name} left, {_quantity.Name} right) {{ - return left.Value > right.ToUnit(left.Unit).Value; + return left.Value > right.As(left.Unit); }} - // We use obsolete attribute to communicate the preferred equality members to use. - // CS0809: Obsolete member 'memberA' overrides non-obsolete member 'memberB'. - #pragma warning disable CS0809 - - /// Indicates strict equality of two quantities, where both and are exactly equal. - [Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For equality checks, use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")] + /// + /// Determines whether two instances are equal. + /// + /// + /// Equality is evaluated in a unit-aware manner. The right-hand operand is converted to the unit of the left-hand + /// operand and then the underlying numeric values are compared. + /// This means two quantities with numerically equal values but different units will be considered equal. + /// The operator delegates to , which implements this conversion-and-compare logic. + /// public static bool operator ==({_quantity.Name} left, {_quantity.Name} right) {{ return left.Equals(right); }} - /// Indicates strict inequality of two quantities, where both and are exactly equal. - [Obsolete(""For null checks, use `x is null` syntax to not invoke overloads. For equality checks, use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")] + /// + /// Determines whether two instances are not equal. + /// + /// + /// This operator is the logical negation of . + /// See that operator (and ) for details on how equality is evaluated + /// (i.e., by converting one operand to the other's unit and comparing their numeric values). + /// public static bool operator !=({_quantity.Name} left, {_quantity.Name} right) {{ return !(left == right); }} /// - /// Indicates strict equality of two quantities, where both and are exactly equal. - [Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")] + /// + /// Determines whether the specified object is equal to the current instance. + /// + /// + /// Returns false if is null or not a . + /// When is a , this method delegates to + /// , which performs a unit-aware comparison by converting the other + /// instance to this instance's unit before comparing numeric values. + /// public override bool Equals(object? obj) {{ - if (obj is null || !(obj is {_quantity.Name} otherQuantity)) + if (obj is not {_quantity.Name} otherQuantity) return false; return Equals(otherQuantity); }} /// - /// Indicates strict equality of two quantities, where both and are exactly equal. - [Obsolete(""Use Equals({_quantity.Name} other, {_quantity.Name} tolerance) instead, to check equality across units and to specify the max tolerance for rounding errors due to floating-point arithmetic when converting between units."")] + /// + /// Determines whether the current instance is equal to another instance. + /// + /// + /// Comparison is performed by converting to this instance's unit and then comparing the underlying numeric values. + /// This makes two quantities equal even when their units differ, provided the converted numeric values are equal. + /// public bool Equals({_quantity.Name} other) {{ - return new {{ Value, Unit }}.Equals(new {{ other.Value, other.Unit }}); + return _value.Equals(other.As(this.Unit)); }} - #pragma warning restore CS0809 - /// /// Returns the hash code for this instance. /// /// A hash code for the current {_quantity.Name}. public override int GetHashCode() {{ - return Comparison.GetHashCode(Unit, Value); + return Comparison.GetHashCode(typeof({_quantity.Name}), this.As(BaseUnit)); }} - /// Compares the current with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other when converted to the same unit. + /// /// An object to compare with this instance. /// /// is not the same type as this instance. /// - /// A value that indicates the relative order of the quantities being compared. The return value has these meanings: - /// - /// Value Meaning - /// Less than zero This instance precedes in the sort order. - /// Zero This instance occurs in the same position in the sort order as . - /// Greater than zero This instance follows in the sort order. - /// - /// public int CompareTo(object? obj) {{ - if (obj is null) throw new ArgumentNullException(nameof(obj)); - if (!(obj is {_quantity.Name} otherQuantity)) throw new ArgumentException(""Expected type {_quantity.Name}."", nameof(obj)); + if (obj is not {_quantity.Name} otherQuantity) + throw obj is null ? new ArgumentNullException(nameof(obj)) : ExceptionHelper.CreateArgumentException<{_quantity.Name}>(obj, nameof(obj)); return CompareTo(otherQuantity); }} - /// Compares the current with another and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other when converted to the same unit. + /// + /// Compares the current with another and returns an integer that indicates + /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the other quantity, when converted to the same unit. + /// /// A quantity to compare with this instance. /// A value that indicates the relative order of the quantities being compared. The return value has these meanings: /// @@ -997,162 +1007,9 @@ public int CompareTo(object? obj) /// public int CompareTo({_quantity.Name} other) {{ - return _value.CompareTo(other.ToUnit(this.Unit).Value); - }} - - #endregion -"); - } - - private void GenerateConversionMethods() - { - Writer.WL($@" - #region Conversion Methods - - /// - /// Convert to the unit representation . - /// - /// Value converted to the specified unit. - public double As({_unitEnumName} unit) - {{ - if (Unit == unit) - return Value; - - return ToUnit(unit).Value; - }} -"); - - Writer.WL( $@" - - /// - public double As(UnitKey unitKey) - {{ - return As(unitKey.ToUnit<{_unitEnumName}>()); - }} -"); - - Writer.WL($@" - /// - /// Converts this {_quantity.Name} to another {_quantity.Name} with the unit representation . - /// - /// The unit to convert to. - /// A {_quantity.Name} with the specified unit. - public {_quantity.Name} ToUnit({_unitEnumName} unit) - {{ - return ToUnit(unit, DefaultConversionFunctions); - }} - - /// - /// Converts this to another using the given with the unit representation . - /// - /// The unit to convert to. - /// The to use for the conversion. - /// A {_quantity.Name} with the specified unit. - public {_quantity.Name} ToUnit({_unitEnumName} unit, UnitConverter unitConverter) - {{ - if (TryToUnit(unit, out var converted)) - {{ - // Try to convert using the auto-generated conversion methods. - return converted!.Value; - }} - else if (unitConverter.TryGetConversionFunction((typeof({_quantity.Name}), Unit, typeof({_quantity.Name}), unit), out var conversionFunction)) - {{ - // See if the unit converter has an extensibility conversion registered. - return ({_quantity.Name})conversionFunction(this); - }} - else if (Unit != BaseUnit) - {{ - // Conversion to requested unit NOT found. Try to convert to BaseUnit, and then from BaseUnit to requested unit. - var inBaseUnits = ToUnit(BaseUnit); - return inBaseUnits.ToUnit(unit); - }} - else - {{ - // No possible conversion - throw new UnitNotFoundException($""Can't convert {{Unit}} to {{unit}}.""); - }} - }} - - /// - /// Attempts to convert this to another with the unit representation . - /// - /// The unit to convert to. - /// The converted in , if successful. - /// True if successful, otherwise false. - private bool TryToUnit({_unitEnumName} unit, [NotNullWhen(true)] out {_quantity.Name}? converted) - {{ - if (Unit == unit) - {{ - converted = this; - return true; - }} - - {_quantity.Name}? convertedOrNull = (Unit, unit) switch - {{ - // {_unitEnumName} -> BaseUnit"); - - foreach (Unit unit in _quantity.Units) - { - if (unit.SingularName == _quantity.BaseUnit) continue; - - var func = unit.FromUnitToBaseFunc.Replace("{x}", "_value"); - Writer.WL($@" - ({_unitEnumName}.{unit.SingularName}, {_unitEnumName}.{_quantity.BaseUnit}) => new {_quantity.Name}({func}, {_unitEnumName}.{_quantity.BaseUnit}),"); - } - - Writer.WL(); - Writer.WL($@" - - // BaseUnit -> {_unitEnumName}"); - foreach(Unit unit in _quantity.Units) - { - if (unit.SingularName == _quantity.BaseUnit) continue; - - var func = unit.FromBaseToUnitFunc.Replace("{x}", "_value"); - Writer.WL($@" - ({_unitEnumName}.{_quantity.BaseUnit}, {_unitEnumName}.{unit.SingularName}) => new {_quantity.Name}({func}, {_unitEnumName}.{unit.SingularName}),"); - } - - Writer.WL(); - Writer.WL($@" - _ => null - }}; - - if (convertedOrNull is null) - {{ - converted = default; - return false; - }} - - converted = convertedOrNull.Value; - return true; - }} -"); - Writer.WL($@" - #region Explicit implementations - - double IQuantity.As(Enum unit) - {{ - if (unit is not {_unitEnumName} typedUnit) - throw new ArgumentException($""The given unit is of type {{unit.GetType()}}. Only {{typeof({_unitEnumName})}} is supported."", nameof(unit)); - - return As(typedUnit); - }} - - /// - IQuantity IQuantity.ToUnit(Enum unit) - {{ - if (!(unit is {_unitEnumName} typedUnit)) - throw new ArgumentException($""The given unit is of type {{unit.GetType()}}. Only {{typeof({_unitEnumName})}} is supported."", nameof(unit)); - - return ToUnit(typedUnit, DefaultConversionFunctions); + return _value.CompareTo(other.As(this.Unit)); }} - /// - IQuantity<{_unitEnumName}> IQuantity<{_unitEnumName}>.ToUnit({_unitEnumName} unit) => ToUnit(unit); - - #endregion - #endregion "); } @@ -1171,11 +1028,13 @@ public override string ToString() return ToString(null, null); }} - /// + /// /// /// Gets the string representation of this instance in the specified format string using the specified format provider, or if null. /// - public string ToString(string? format, IFormatProvider? provider) + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] string? format, + IFormatProvider? provider) {{ return QuantityFormatter.Default.Format(this, format, provider); }} diff --git a/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs b/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs index 64479dbfa2..b2d565a324 100644 --- a/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/StaticQuantityGenerator.cs @@ -1,4 +1,6 @@ -using CodeGen.JsonTypes; +using System.Collections.Generic; +using System.Linq; +using CodeGen.JsonTypes; namespace CodeGen.Generators.UnitsNetGen { @@ -33,27 +35,46 @@ internal static class DefaultProvider /// /// All QuantityInfo instances that are present in UnitsNet by default. /// - internal static IReadOnlyList Quantities { get; } = - ["); + internal static IReadOnlyList Quantities => new QuantityInfo[] + {"); foreach (var quantity in _quantities) Writer.WL($@" {quantity.Name}.Info,"); Writer.WL(@" - ]; + }; - internal static void RegisterUnitConversions(UnitConverter unitConverter) + /// + /// All implicit quantity conversions that exist by default. + /// + internal static readonly IReadOnlyList Conversions = new QuantityConversionMapping[] {"); - foreach (Quantity quantity in _quantities) - { + foreach (var quantityRelation in _quantities.SelectMany(quantity => quantity.Relations.Where(x => x.Operator == "inverse")).Distinct(new CumulativeRelationshipEqualityComparer()).OrderBy(relation => relation.LeftQuantity.Name)) Writer.WL($@" - {quantity.Name}.RegisterDefaultConversions(unitConverter);"); - } - + new (typeof({quantityRelation.LeftQuantity.Name}), typeof({quantityRelation.RightQuantity.Name})),"); Writer.WL(@" - } + }; } }"); return Writer.ToString(); } } + + internal class CumulativeRelationshipEqualityComparer: IEqualityComparer{ + public bool Equals(QuantityRelation? x, QuantityRelation? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null) return false; + if (y is null) return false; + if (x.GetType() != y.GetType()) return false; + return + x.ResultQuantity == y.ResultQuantity && ( + (x.LeftQuantity.Equals(y.LeftQuantity) && x.RightQuantity.Equals(y.RightQuantity)) + || (x.LeftQuantity.Equals(y.RightQuantity) && x.RightQuantity.Equals(y.LeftQuantity))); + } + + public int GetHashCode(QuantityRelation obj) + { + return obj.LeftQuantity.GetHashCode() ^ obj.RightQuantity.GetHashCode(); + } + } } diff --git a/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs b/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs index a5818b07cf..031ac986ab 100644 --- a/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs +++ b/CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -59,7 +59,7 @@ internal class UnitTestBaseClassGenerator : GeneratorBase /// A dimensionless quantity has all base dimensions (L, M, T, I, Θ, N, J) equal to zero. /// private readonly bool _isDimensionless; - + /// /// Stores a mapping of culture names to their corresponding unique unit abbreviations. /// Each culture maps to a dictionary where the key is the unit abbreviation and the value is the corresponding @@ -101,7 +101,7 @@ internal class UnitTestBaseClassGenerator : GeneratorBase /// is not available for the defined unit localizations. /// private const string FallbackCultureName = "en-US"; - + public UnitTestBaseClassGenerator(Quantity quantity) { _quantity = quantity; @@ -263,6 +263,7 @@ public abstract partial class {_quantity.Name}TestsBase : QuantityTestsBase Writer.WL($@" new object[] {{ {GetUnitFullName(unit)} }},"); } + Writer.WL($@" }}; @@ -339,6 +340,20 @@ public void Ctor_UnitSystem_ThrowsArgumentExceptionIfNotSupported() Assert.Equal(quantityInfo, ((IQuantity<{_unitEnumName}>)quantity).QuantityInfo); }} + [Fact] + public void {_quantity.Name}Info_CreateWithCustomUnitInfos() + {{ + {_unitEnumName}[] expectedUnits = [{_baseUnitFullName}]; + + {_quantity.Name}.{_quantity.Name}Info quantityInfo = {_quantity.Name}.{_quantity.Name}Info.CreateDefault(mappings => mappings.SelectUnits(expectedUnits)); + + Assert.Equal(""{_quantity.Name}"", quantityInfo.Name); + Assert.Equal({_quantity.Name}.Zero, quantityInfo.Zero); + Assert.Equal({_quantity.Name}.BaseUnit, quantityInfo.BaseUnitInfo.Value); + Assert.Equal(expectedUnits, quantityInfo.Units); + Assert.Equal(expectedUnits, quantityInfo.UnitInfos.Select(x => x.Value)); + }} + [Fact] public void {_baseUnit.SingularName}To{_quantity.Name}Units() {{ @@ -412,12 +427,31 @@ public void As_UnitSystem_ThrowsArgumentNullExceptionIfNull() [Fact] public void ToUnit_UnitSystem_ReturnsValueInDimensionlessUnit() {{ - var quantity = new {_quantity.Name}(value: 1, unit: {_baseUnitFullName}); + Assert.Multiple(() => + {{ + var quantity = new {_quantity.Name}(value: 1, unit: {_baseUnitFullName}); - {_quantity.Name} convertedQuantity = quantity.ToUnit(UnitSystem.SI); + {_quantity.Name} convertedQuantity = quantity.ToUnit(UnitSystem.SI); - Assert.Equal({_baseUnitFullName}, convertedQuantity.Unit); - Assert.Equal(quantity.Value, convertedQuantity.Value); + Assert.Equal({_baseUnitFullName}, convertedQuantity.Unit); + Assert.Equal(quantity.Value, convertedQuantity.Value); + }}, () => + {{ + IQuantity<{_unitEnumName}> quantity = new {_quantity.Name}(value: 1, unit: {_baseUnitFullName}); + + IQuantity<{_unitEnumName}> convertedQuantity = quantity.ToUnit(UnitSystem.SI); + + Assert.Equal({_baseUnitFullName}, convertedQuantity.Unit); + Assert.Equal(quantity.Value, convertedQuantity.Value); + }}, () => + {{ + IQuantity quantity = new {_quantity.Name}(value: 1, unit: {_baseUnitFullName}); + + IQuantity convertedQuantity = quantity.ToUnit(UnitSystem.SI); + + Assert.Equal({_baseUnitFullName}, convertedQuantity.Unit); + Assert.Equal(quantity.Value, convertedQuantity.Value); + }}); }} [Fact] @@ -485,26 +519,69 @@ public virtual void ToUnit_UnitSystem_SI_ReturnsQuantityInSIUnits() var expectedUnit = {_quantity.Name}.Info.GetDefaultUnit(UnitSystem.SI); var expectedValue = quantity.As(expectedUnit); - {_quantity.Name} convertedQuantity = quantity.ToUnit(UnitSystem.SI); + Assert.Multiple(() => + {{ + {_quantity.Name} quantityToConvert = quantity; + + {_quantity.Name} convertedQuantity = quantityToConvert.ToUnit(UnitSystem.SI); + + Assert.Equal(expectedUnit, convertedQuantity.Unit); + Assert.Equal(expectedValue, convertedQuantity.Value); + }}, () => + {{ + IQuantity<{_unitEnumName}> quantityToConvert = quantity; + + IQuantity<{_unitEnumName}> convertedQuantity = quantityToConvert.ToUnit(UnitSystem.SI); - Assert.Equal(expectedUnit, convertedQuantity.Unit); - Assert.Equal(expectedValue, convertedQuantity.Value); + Assert.Equal(expectedUnit, convertedQuantity.Unit); + Assert.Equal(expectedValue, convertedQuantity.Value); + }}, () => + {{ + IQuantity quantityToConvert = quantity; + + IQuantity convertedQuantity = quantityToConvert.ToUnit(UnitSystem.SI); + + Assert.Equal(expectedUnit, convertedQuantity.Unit); + Assert.Equal(expectedValue, convertedQuantity.Value); + }}); }} [Fact] public void ToUnit_UnitSystem_ThrowsArgumentNullExceptionIfNull() {{ UnitSystem nullUnitSystem = null!; - var quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); - Assert.Throws(() => quantity.ToUnit(nullUnitSystem)); + Assert.Multiple(() => + {{ + var quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); + Assert.Throws(() => quantity.ToUnit(nullUnitSystem)); + }}, () => + {{ + IQuantity<{_unitEnumName}> quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); + Assert.Throws(() => quantity.ToUnit(nullUnitSystem)); + }}, () => + {{ + IQuantity quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); + Assert.Throws(() => quantity.ToUnit(nullUnitSystem)); + }}); }} [Fact] public void ToUnit_UnitSystem_ThrowsArgumentExceptionIfNotSupported() {{ var unsupportedUnitSystem = new UnitSystem(UnsupportedBaseUnits); - var quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); - Assert.Throws(() => quantity.ToUnit(unsupportedUnitSystem)); + Assert.Multiple(() => + {{ + var quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); + Assert.Throws(() => quantity.ToUnit(unsupportedUnitSystem)); + }}, () => + {{ + IQuantity<{_unitEnumName}> quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); + Assert.Throws(() => quantity.ToUnit(unsupportedUnitSystem)); + }}, () => + {{ + IQuantity quantity = new {_quantity.Name}(value: 1, unit: {_quantity.Name}.BaseUnit); + Assert.Throws(() => quantity.ToUnit(unsupportedUnitSystem)); + }}); }} "); } @@ -522,7 +599,7 @@ public void ToUnit_UnitSystem_ThrowsArgumentExceptionIfNotSupported() } Writer.WL($@" - public void Parse(string culture, string quantityString, {_unitEnumName} expectedUnit, double expectedValue) + public void Parse(string culture, string quantityString, {_unitEnumName} expectedUnit, decimal expectedValue) {{ using var _ = new CultureScope(culture); var parsed = {_quantity.Name}.Parse(quantityString); @@ -567,7 +644,7 @@ public void ParseWithAmbiguousAbbreviation(string culture, string quantityString } Writer.WL($@" - public void TryParse(string culture, string quantityString, {_unitEnumName} expectedUnit, double expectedValue) + public void TryParse(string culture, string quantityString, {_unitEnumName} expectedUnit, decimal expectedValue) {{ using var _ = new CultureScope(culture); Assert.True({_quantity.Name}.TryParse(quantityString, out {_quantity.Name} parsed)); @@ -825,7 +902,7 @@ public void GetAbbreviationWithDefaultCulture() }}); }} "); - + Writer.WL($@" [Theory] [MemberData(nameof(UnitTypes))] @@ -857,6 +934,7 @@ public void ToUnit_FromNonBaseUnit_ReturnsQuantityWithGivenUnit({_unitEnumName} var quantity = {_quantity.Name}.From(3.0, fromUnit); var converted = quantity.ToUnit(unit); Assert.Equal(converted.Unit, unit); + Assert.Equal(quantity, converted); }}); }} @@ -880,20 +958,22 @@ public void ToUnit_FromIQuantity_ReturnsTheExpectedIQuantity({_unitEnumName} uni IQuantity<{_unitEnumName}> quantityToConvert = quantity; IQuantity<{_unitEnumName}> convertedQuantity = quantityToConvert.ToUnit(unit); Assert.Equal(unit, convertedQuantity.Unit); + Assert.Equal(expectedQuantity, convertedQuantity); }}, () => {{ IQuantity quantityToConvert = quantity; IQuantity convertedQuantity = quantityToConvert.ToUnit(unit); Assert.Equal(unit, convertedQuantity.Unit); + Assert.Equal(expectedQuantity, convertedQuantity); }}); }} [Fact] public void ConversionRoundTrip() {{ - {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(1);"); + {_quantity.Name} {baseUnitVariableName} = {_quantity.Name}.From{_baseUnit.PluralName}(3);"); foreach (var unit in _quantity.Units) Writer.WL($@" - AssertEx.EqualTolerance(1, {_quantity.Name}.From{unit.PluralName}({baseUnitVariableName}.{unit.PluralName}).{_baseUnit.PluralName}, {unit.PluralName}Tolerance);"); + Assert.Equal(3, {_quantity.Name}.From{unit.PluralName}({baseUnitVariableName}.{unit.PluralName}).{_baseUnit.PluralName});"); Writer.WL($@" }} "); @@ -905,13 +985,13 @@ public void ConversionRoundTrip() public void LogarithmicArithmeticOperators() {{ {_quantity.Name} v = {_quantity.Name}.From{_baseUnit.PluralName}(40); - AssertEx.EqualTolerance(-40, -v.{_baseUnit.PluralName}, {unit.PluralName}Tolerance); + Assert.Equal(-40, -v.{_baseUnit.PluralName}); AssertLogarithmicAddition(); AssertLogarithmicSubtraction(); - AssertEx.EqualTolerance(50, (v*10).{_baseUnit.PluralName}, {unit.PluralName}Tolerance); - AssertEx.EqualTolerance(50, (10*v).{_baseUnit.PluralName}, {unit.PluralName}Tolerance); - AssertEx.EqualTolerance(35, (v/5).{_baseUnit.PluralName}, {unit.PluralName}Tolerance); - AssertEx.EqualTolerance(35, v/{_quantity.Name}.From{_baseUnit.PluralName}(5), {unit.PluralName}Tolerance); + Assert.Equal(50, (v * 10).{_baseUnit.PluralName}); + Assert.Equal(50, (10 * v).{_baseUnit.PluralName}); + Assert.Equal(35, (v / 5).{_baseUnit.PluralName}); + Assert.Equal(35, v / {_quantity.Name}.From{_baseUnit.PluralName}(5)); }} protected abstract void AssertLogarithmicAddition(); @@ -926,13 +1006,13 @@ public void LogarithmicArithmeticOperators() public void ArithmeticOperators() {{ {_quantity.Name} v = {_quantity.Name}.From{_baseUnit.PluralName}(1); - AssertEx.EqualTolerance(-1, -v.{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance); - AssertEx.EqualTolerance(2, ({_quantity.Name}.From{_baseUnit.PluralName}(3)-v).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance); - AssertEx.EqualTolerance(2, (v + v).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance); - AssertEx.EqualTolerance(10, (v*10).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance); - AssertEx.EqualTolerance(10, (10*v).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance); - AssertEx.EqualTolerance(2, ({_quantity.Name}.From{_baseUnit.PluralName}(10)/5).{_baseUnit.PluralName}, {_baseUnit.PluralName}Tolerance); - AssertEx.EqualTolerance(2, {_quantity.Name}.From{_baseUnit.PluralName}(10)/{_quantity.Name}.From{_baseUnit.PluralName}(5), {_baseUnit.PluralName}Tolerance); + Assert.Equal(-1, -v.{_baseUnit.PluralName}); + Assert.Equal(2, ({_quantity.Name}.From{_baseUnit.PluralName}(3) - v).{_baseUnit.PluralName}); + Assert.Equal(2, (v + v).{_baseUnit.PluralName}); + Assert.Equal(10, (v * 10).{_baseUnit.PluralName}); + Assert.Equal(10, (10 * v).{_baseUnit.PluralName}); + Assert.Equal(2, ({_quantity.Name}.From{_baseUnit.PluralName}(10) / 5).{_baseUnit.PluralName}); + Assert.Equal(2, {_quantity.Name}.From{_baseUnit.PluralName}(10) / {_quantity.Name}.From{_baseUnit.PluralName}(5)); }} "); } @@ -985,13 +1065,6 @@ public void CompareToThrowsOnNull() [Theory] [InlineData(1, {_baseUnitFullName}, 1, {_baseUnitFullName}, true)] // Same value and unit. [InlineData(1, {_baseUnitFullName}, 2, {_baseUnitFullName}, false)] // Different value. - [InlineData(2, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different value and unit."); - if (_baseUnit != _otherOrBaseUnit) - { - Writer.WL($@" - [InlineData(1, {_baseUnitFullName}, 1, {_otherOrBaseUnitFullName}, false)] // Different unit."); - } - Writer.WL($@" public void Equals_ReturnsTrue_IfValueAndUnitAreEqual(double valueA, {_unitEnumName} unitA, double valueB, {_unitEnumName} unitB, bool expectEqual) {{ var a = new {_quantity.Name}(valueA, unitA); @@ -1056,8 +1129,8 @@ public void Equals_Logarithmic_WithTolerance(double firstValue, double secondVal var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(firstValue); var otherQuantity = {_quantity.Name}.From{_baseUnit.PluralName}(secondValue); {differenceResultType} maxTolerance = quantity > otherQuantity ? quantity - otherQuantity : otherQuantity - quantity; - var largerTolerance = maxTolerance * 1.1; - var smallerTolerance = maxTolerance / 1.1; + var largerTolerance = maxTolerance * 1.1m; + var smallerTolerance = maxTolerance / 1.1m; Assert.True(quantity.Equals(quantity, {differenceResultType}.Zero)); Assert.True(quantity.Equals(quantity, maxTolerance)); Assert.True(quantity.Equals(otherQuantity, largerTolerance)); @@ -1089,8 +1162,8 @@ public void Equals_WithTolerance(double firstValue, double secondValue) var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(firstValue); var otherQuantity = {_quantity.Name}.From{_baseUnit.PluralName}(secondValue); {differenceResultType} maxTolerance = quantity > otherQuantity ? quantity - otherQuantity : otherQuantity - quantity; - var largerTolerance = maxTolerance * 1.1; - var smallerTolerance = maxTolerance / 1.1; + var largerTolerance = maxTolerance * 1.1m; + var smallerTolerance = maxTolerance / 1.1m; Assert.True(quantity.Equals(quantity, {differenceResultType}.Zero)); Assert.True(quantity.Equals(quantity, maxTolerance)); Assert.True(quantity.Equals(otherQuantity, maxTolerance)); @@ -1131,7 +1204,7 @@ public void Equals_WithNegativeTolerance_ThrowsArgumentOutOfRangeException() [Fact] public void HasAtLeastOneAbbreviationSpecified() {{ - var units = Enum.GetValues<{_unitEnumName}>(); + var units = EnumHelper.GetValues<{_unitEnumName}>(); foreach (var unit in units) {{ var defaultAbbreviation = UnitsNetSetup.Default.UnitAbbreviations.GetDefaultAbbreviation(unit); @@ -1144,6 +1217,18 @@ public void BaseDimensionsShouldNeverBeNull() Assert.False({_quantity.Name}.BaseDimensions is null); }} + [Fact] + public void Units_ReturnsTheQuantityInfoUnits() + {{ + Assert.Equal({_quantity.Name}.Info.Units, {_quantity.Name}.Units); + }} + + [Fact] + public void DefaultConversionFunctions_ReturnsTheDefaultUnitConverter() + {{ + Assert.Equal(UnitConverter.Default, {_quantity.Name}.DefaultConversionFunctions); + }} + [Fact] public void ToString_ReturnsValueAndUnitAbbreviationInCurrentCulture() {{ @@ -1170,26 +1255,6 @@ public void ToString_WithSwedishCulture_ReturnsUnitAbbreviationForEnglishCulture Writer.WL($@" }} - [Fact] - public void ToString_SFormat_FormatsNumberWithGivenDigitsAfterRadixForCurrentCulture() - {{ - var _ = new CultureScope(CultureInfo.InvariantCulture); - Assert.Equal(""0.1{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s1"")); - Assert.Equal(""0.12{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s2"")); - Assert.Equal(""0.123{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s3"")); - Assert.Equal(""0.1235{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s4"")); - }} - - [Fact] - public void ToString_SFormatAndCulture_FormatsNumberWithGivenDigitsAfterRadixForGivenCulture() - {{ - var culture = CultureInfo.InvariantCulture; - Assert.Equal(""0.1{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s1"", culture)); - Assert.Equal(""0.12{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s2"", culture)); - Assert.Equal(""0.123{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s3"", culture)); - Assert.Equal(""0.1235{_baseUnitEnglishAbbreviation}"", new {_quantity.Name}(0.123456, {_baseUnitFullName}).ToString(""s4"", culture)); - }} - [Theory] [InlineData(null)] [InlineData(""en-US"")] @@ -1216,7 +1281,8 @@ public void ToString_NullProvider_EqualsCurrentCulture(string format) public void GetHashCode_Equals() {{ var quantity = {_quantity.Name}.From{_baseUnit.PluralName}(1.0); - Assert.Equal(Comparison.GetHashCode(quantity.Unit, quantity.Value), quantity.GetHashCode()); + var expected = Comparison.GetHashCode(typeof({_quantity.Name}), quantity.As({_quantity.Name}.BaseUnit)); + Assert.Equal(expected, quantity.GetHashCode()); }} "); diff --git a/CodeGen/Generators/UnitsNetGenerator.cs b/CodeGen/Generators/UnitsNetGenerator.cs index 7b69071029..a2d5ed9ce6 100644 --- a/CodeGen/Generators/UnitsNetGenerator.cs +++ b/CodeGen/Generators/UnitsNetGenerator.cs @@ -1,10 +1,11 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System.Collections.Generic; using System.IO; using System.Linq; using CodeGen.Generators.UnitsNetGen; +using CodeGen.Helpers; using CodeGen.Helpers.UnitEnumValueAllocation; using CodeGen.JsonTypes; using Serilog; @@ -91,63 +92,63 @@ private static void GenerateQuantityTestClassIfNotExists(Quantity quantity, stri if (File.Exists(filePath)) return; var content = new UnitTestStubGenerator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); Log.Information("✅ {Quantity} initial test stub", quantity.Name); } private static void GenerateQuantity(Quantity quantity, string filePath) { var content = new QuantityGenerator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateNumberToExtensions(Quantity quantity, string filePath) { var content = new NumberExtensionsGenerator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateNumberToExtensionsTestClass(Quantity quantity, string filePath) { var content = new NumberExtensionsTestClassGenerator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateNumberToExtensionsCS14(Quantity quantity, string filePath) { var content = new NumberExtensionsCS14Generator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateNumberToExtensionsCS14TestClass(Quantity quantity, string filePath) { var content = new NumberExtensionsCS14TestClassGenerator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateUnitType(Quantity quantity, string filePath, UnitEnumNameToValue unitEnumValues) { var content = new UnitTypeGenerator(quantity, unitEnumValues).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateQuantityTestBaseClass(Quantity quantity, string filePath) { var content = new UnitTestBaseClassGenerator(quantity).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); } private static void GenerateIQuantityTests(Quantity[] quantities, string filePath) { var content = new IQuantityTestClassGenerator(quantities).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); Log.Information("✅ IQuantityTests.g.cs"); } private static void GenerateStaticQuantity(Quantity[] quantities, string filePath) { var content = new StaticQuantityGenerator(quantities).Generate(); - File.WriteAllText(filePath, content); + CodeGenFile.WriteAllText(filePath, content); Log.Information("✅ Quantity.g.cs"); } @@ -171,10 +172,7 @@ private static void GenerateResourceFiles(Quantity[] quantities, string resource $"{resourcesDirectory}/{quantity.Name}.restext" : $"{resourcesDirectory}/{quantity.Name}.{culture}.restext"; - // Ensure parent folder exists - Directory.CreateDirectory(resourcesDirectory); - - using var writer = File.CreateText(fileName); + using var writer = CodeGenFile.CreateText(fileName); foreach(Unit unit in quantity.Units) { diff --git a/CodeGen/Helpers/CodeGenFile.cs b/CodeGen/Helpers/CodeGenFile.cs new file mode 100644 index 0000000000..54554d979b --- /dev/null +++ b/CodeGen/Helpers/CodeGenFile.cs @@ -0,0 +1,57 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System.IO; +using System.Text; + +namespace CodeGen.Helpers +{ + /// + /// Provides file I/O helpers for CodeGen files with explicit UTF-8 encoding behavior. + /// + internal static class CodeGenFile + { + /// + /// UTF-8 encoding without byte order mark, used for generated and codegen-normalized files. + /// + internal static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + /// + /// Reads all text from a CodeGen input file as UTF-8. + /// + /// + /// Existing byte order marks are still detected when present. + /// + public static string ReadAllText(string path) + { + return File.ReadAllText(path, Utf8NoBom); + } + + /// + /// Writes all text to a generated or codegen-normalized file as UTF-8 without byte order mark. + /// + public static void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents, Utf8NoBom); + } + + /// + /// Opens a CodeGen input file for text reading as UTF-8. + /// + /// + /// Existing byte order marks are still detected when present. + /// + public static StreamReader OpenText(string path) + { + return new StreamReader(path, Utf8NoBom, detectEncodingFromByteOrderMarks: true); + } + + /// + /// Creates or overwrites a generated or codegen-normalized text file as UTF-8 without byte order mark. + /// + public static StreamWriter CreateText(string path) + { + return new StreamWriter(path, append: false, Utf8NoBom); + } + } +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluationTerm.cs b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluationTerm.cs new file mode 100644 index 0000000000..5b17bc933d --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluationTerm.cs @@ -0,0 +1,14 @@ +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer; + +/// +/// A term of the form "P^n" where P is a term that hasn't been parsed, raised to the given power. +/// +/// The actual expression to parse +/// The exponent to use on the parsed expression (default is 1) +/// +/// Since we're tokenizing the expressions from top to bottom, the first step is parsing the exponent of the +/// expression: e.g. Math.Pow(P, 2) +/// +public record ExpressionEvaluationTerm(string Expression, Fraction Exponent); diff --git a/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluator.cs new file mode 100644 index 0000000000..379adcd1d0 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/ExpressionEvaluator.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; +using CodeGen.Helpers.ExpressionAnalyzer.Functions; +using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; +using Fractions; +using static System.Globalization.CultureInfo; + +namespace CodeGen.Helpers.ExpressionAnalyzer; + +internal partial class ExpressionEvaluator // TODO make public (and move out in a separate project) +{ + public static readonly Fraction Pi = Fraction.FromDoubleRounded(Math.PI, 16); + private readonly IReadOnlyDictionary _constantValues; + private readonly Dictionary _expressionsEvaluated = []; + + private readonly IReadOnlyDictionary _functionEvaluators; + + public ExpressionEvaluator(string parameterName, params IFunctionEvaluator[] functionEvaluators) + : this(parameterName, functionEvaluators.ToDictionary(x => x.FunctionName), new Dictionary()) + { + } + + public ExpressionEvaluator(string parameterName, IReadOnlyDictionary constantValues, params IFunctionEvaluator[] functionEvaluators) + : this(parameterName, functionEvaluators.ToDictionary(x => x.FunctionName), constantValues) + { + } + + public ExpressionEvaluator(string parameterName, IReadOnlyDictionary functionEvaluators, + IReadOnlyDictionary constantValues) + { + ParameterName = parameterName; + _constantValues = constantValues; + _functionEvaluators = functionEvaluators; + } + + public string ParameterName { get; } + + protected string Add(CompositeExpression expression) + { + var label = "{" + (char)('a' + _expressionsEvaluated.Count) + "}"; + _expressionsEvaluated[label] = expression; + return label; + } + + public CompositeExpression Evaluate(ExpressionEvaluationTerm expressionEvaluationTerm) // TODO either replace by string or add a coefficient + { + if (TryParseExpressionTerm(expressionEvaluationTerm.Expression, expressionEvaluationTerm.Exponent, out ExpressionTerm? expressionTerm)) + { + return expressionTerm; + } + + var expressionToParse = expressionEvaluationTerm.Expression; + Fraction exponent = expressionEvaluationTerm.Exponent; + string previousExpression; + do + { + previousExpression = expressionToParse; + // the regex captures the innermost occurrence of a function group: "Sin(x)", "Pow(x, y)", "(x + 1)" are all valid matches + expressionToParse = MathOperationRegex().Replace(expressionToParse, match => + { + var functionName = match.Groups[1].Value; + var functionBodyToParse = match.Groups[2].Value; + var evaluationTerm = new ExpressionEvaluationTerm(functionBodyToParse, exponent); + if (string.IsNullOrEmpty(functionName)) // standard grouping (technically this is equivalent to f(x) -> x) + { + // all terms within the group are expanded: extract the simplified expression + CompositeExpression expression = ReplaceTokenizedExpressions(evaluationTerm); + return Add(expression); + } + + if (_functionEvaluators.TryGetValue(functionName, out IFunctionEvaluator? functionEvaluator)) + { + // resolve the expression using the custom function evaluator + CompositeExpression expression = functionEvaluator.CreateExpression(evaluationTerm, ReplaceTokenizedExpressions); + return Add(expression); + } + + throw new FormatException($"No function evaluator available for {functionName}({functionBodyToParse})"); + }); + } while (previousExpression != expressionToParse); + + return ReplaceTokenizedExpressions(expressionEvaluationTerm with { Expression = expressionToParse }); + } + + private CompositeExpression ReplaceTokenizedExpressions(ExpressionEvaluationTerm tokenizedExpression) + { + // all groups and function are expanded: we're left with a standard arithmetic expression such as "4 * a + 2 * b * x - c - d + 5" + // with a, b, c, d representing the previously evaluated expressions + var result = new CompositeExpression(); + var stringBuilder = new StringBuilder(); + ArithmeticOperationToken lastToken = ArithmeticOperationToken.Addition; + CompositeExpression? runningExpression = null; + foreach (var character in tokenizedExpression.Expression) + { + if (!TryReadToken(character, out ArithmeticOperationToken currentToken)) // TODO use None? + { + continue; + } + + switch (currentToken) + { + case ArithmeticOperationToken.Addition or ArithmeticOperationToken.Subtraction: + { + if (stringBuilder.Length == 0) // ignore the leading sign + { + lastToken = currentToken; + continue; + } + + // we're at the end of a term expression + CompositeExpression lastTerm = ParseTerm(); + if (runningExpression is null) + { + result.AddTerms(lastTerm); + } + else // the last term is part of a running multiplication + { + result.AddTerms(runningExpression * lastTerm); + runningExpression = null; + } + + lastToken = currentToken; + break; + } + case ArithmeticOperationToken.Multiplication or ArithmeticOperationToken.Division: + { + CompositeExpression previousTerm = ParseTerm(); + if (runningExpression is null) + { + runningExpression = previousTerm; + } + else // the previousTerm term is part of a running multiplication (which is going to be followed by at least one more multiplication/division) + { + runningExpression *= previousTerm; + } + + lastToken = currentToken; + break; + } + } + } + + CompositeExpression finalTerm = ParseTerm(); + if (runningExpression is null) + { + result.AddTerms(finalTerm); + } + else + { + result.AddTerms(runningExpression * finalTerm); + } + + return result; + + bool TryReadToken(char character, out ArithmeticOperationToken token) + { + switch (character) + { + case '+': + token = ArithmeticOperationToken.Addition; + return true; + case '-': + token = ArithmeticOperationToken.Subtraction; + return true; + case '*': + token = ArithmeticOperationToken.Multiplication; + return true; + case '/': + token = ArithmeticOperationToken.Division; + return true; + case not ' ': + stringBuilder.Append(character); + break; + } + + token = default; + return false; + } + + CompositeExpression ParseTerm() + { + var previousExpression = stringBuilder.ToString(); + stringBuilder.Clear(); + if (_expressionsEvaluated.TryGetValue(previousExpression, out CompositeExpression? expression)) + { + return lastToken switch + { + ArithmeticOperationToken.Subtraction => expression.Negate(), + ArithmeticOperationToken.Division => expression.Invert(), + _ => expression + }; + } + + if (TryParseExpressionTerm(previousExpression, tokenizedExpression.Exponent, out ExpressionTerm? expressionTerm)) + { + return lastToken switch + { + ArithmeticOperationToken.Subtraction => expressionTerm.Negate(), + ArithmeticOperationToken.Division => expressionTerm.Invert(), + _ => expressionTerm + }; + } + + throw new FormatException($"Failed to parse the previous token: {previousExpression}"); + } + } + + + private bool TryParseExpressionTerm(string expressionToParse, Fraction exponent, [MaybeNullWhen(false)] out ExpressionTerm expressionTerm) + { + if (expressionToParse == ParameterName) + { + expressionTerm = new ExpressionTerm(Fraction.One, exponent); + return true; + } + + if (_constantValues.TryGetValue(expressionToParse, out Fraction constantExpression) || FractionHelper.TryParseInvariant(expressionToParse, out constantExpression)) + { + expressionTerm = ExpressionTerm.Constant(constantExpression.Pow(exponent)); + return true; + } + + expressionTerm = null; + return false; + } + + public static string ReplaceDecimalNotations(string expression, Dictionary constantValues) + { + return ScientificNotationRegex().Replace(expression, match => + { + var tokens = match.Value.ToLower().Replace("d", "").Split('e'); + if (tokens.Length != 2 || !FractionHelper.TryParseInvariant(tokens[0], out Fraction mantissa) || !int.TryParse(tokens[1], InvariantCulture, out var exponent)) + { + throw new FormatException($"The expression contains invalid tokens: {expression}"); + } + + var label = $"{{v{constantValues.Count}}}"; + constantValues[label] = mantissa * Fraction.Pow(10, exponent); + return label; + }).Replace("d", string.Empty); // TODO these are force-generated for the BitRate (we should stop doing it) + } + + public static string ReplaceMathPi(string expression, Dictionary constantValues) + { + return MathPiRegex().Replace(expression, _ => + { + constantValues[nameof(Pi)] = Pi; + return nameof(Pi); + }); + } + + public static CompositeExpression Evaluate(string expression, string parameter) + { + var constantExpressions = new Dictionary(); + + expression = ReplaceDecimalNotations(expression, constantExpressions); // TODO expose an IPreprocessor (or something) + expression = ReplaceMathPi(expression, constantExpressions); + expression = expression.Replace("Math.", string.Empty); + + // these are no longer necessary + // var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions, + // new SqrtFunctionEvaluator(), + // new PowFunctionEvaluator(), + // new SinFunctionEvaluator(), + // new AsinFunctionEvaluator()); + var expressionEvaluator = new ExpressionEvaluator(parameter, constantExpressions); + + return expressionEvaluator.Evaluate(new ExpressionEvaluationTerm(expression, Fraction.One)); + } + + private enum ArithmeticOperationToken + { + Addition, + Subtraction, + Multiplication, + Division + } + + /// + /// Matches numbers in scientific notation, optionally with a trailing d or D. + /// + /// + /// 1.23e4 + /// 5.67E-8d + /// 0.00123e+3D + /// + [GeneratedRegex(@"\d*(\.\d*)?[eE][-\+]?\d*[dD]?")] + private static partial Regex ScientificNotationRegex(); + + /// + /// Matches occurrences of "Math.PI". + /// + [GeneratedRegex(@"Math\.PI")] + private static partial Regex MathPiRegex(); + + /// + /// Matches the innermost function call or parenthesized group. + /// Group 1: optional function name (e.g. "Sin" or "Pow"); empty for plain parentheses. + /// Group 2: the contents of the parentheses (does not allow nested parentheses). + /// Examples: Sin(x) -> group1="Sin", group2="x"; Pow(x, y) -> group1="Pow", group2="x, y"; (x + 1) -> group1="", group2="x + 1". + /// Use iteratively to find and replace innermost groups first. + /// + [GeneratedRegex(@"(\w*)\(([^()]*)\)")] + private static partial Regex MathOperationRegex(); +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CompositeExpression.cs b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CompositeExpression.cs new file mode 100644 index 0000000000..60bbd7c077 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CompositeExpression.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Expressions; + +/// +/// A set of terms, ordered by their degree: "P(x)^2 + P(x) + 1" +/// +internal class CompositeExpression : IEnumerable +{ + private readonly SortedSet _terms; + + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor creates an empty CompositeExpression with terms sorted in descending order. + /// + public CompositeExpression() + { + _terms = new SortedSet(DescendingOrderComparer); + } + + /// + /// Initializes a new instance of the class with a single term. + /// + /// The initial term of the composite expression. + /// + /// This constructor creates a CompositeExpression with a single term, sorted in descending order. + /// + public CompositeExpression(ExpressionTerm term) + { + _terms = new SortedSet(DescendingOrderComparer) { term }; + } + + private CompositeExpression(IEnumerable terms) + { + _terms = new SortedSet(terms, DescendingOrderComparer); + } + + public Fraction Degree => _terms.Min?.Exponent ?? Fraction.Zero; + + public bool IsConstant => Degree == Fraction.Zero; + + public IReadOnlyCollection Terms => _terms; + + public void Add(ExpressionTerm term) + { + if (_terms.TryGetValue(term, out ExpressionTerm? sameDegreeTerm)) + { + // merge the two terms + term = term with { Coefficient = sameDegreeTerm.Coefficient + term.Coefficient }; + _terms.Remove(sameDegreeTerm); + } + + _terms.Add(term); + } + + public void AddTerms(IEnumerable expressionTerms) + { + foreach (ExpressionTerm term in expressionTerms) + { + Add(term); + } + } + + + public static implicit operator CompositeExpression(ExpressionTerm term) + { + return new CompositeExpression(term); + } + + public static explicit operator ExpressionTerm(CompositeExpression term) + { + return term._terms.Max!; + } + + public CompositeExpression Negate() + { + return new CompositeExpression(_terms.Select(term => term.Negate())); + } + + public CompositeExpression Invert() + { + return new CompositeExpression(_terms.Select(term => term.Invert())); + } + + public CompositeExpression SolveForY() + { + if (_terms.Count == 0) + { + throw new InvalidOperationException("The expression is empty"); + } + + if (_terms.Count > 2) + { + throw new NotImplementedException("Solving is only supported for expressions of first degree"); + } + + ExpressionTerm degreeTerm = _terms.Min!; + if (degreeTerm.Exponent == Fraction.One) + { + return new CompositeExpression(_terms.Where(x => x.IsConstant).Select(x => x with { Coefficient = x.Coefficient.Negate() / degreeTerm.Coefficient }) + .Prepend(new ExpressionTerm(degreeTerm.Coefficient.Reciprocal(), 1))); + } + + if (degreeTerm.Exponent == Fraction.MinusOne) + { + return new CompositeExpression(_terms.Where(x => x.IsConstant).Select(x => x with { Coefficient = degreeTerm.Coefficient / x.Coefficient.Negate() }) + .Prepend(new ExpressionTerm(degreeTerm.Coefficient, -1))); + } + + throw new NotImplementedException("Solving is only supported for expressions of first degree"); + } + + public CompositeExpression Multiply(CompositeExpression other) + { + var result = new CompositeExpression(); + foreach (ExpressionTerm otherTerm in other) + { + result.AddTerms(_terms.Select(x => x.Multiply(otherTerm))); + } + + return result; + } + + public CompositeExpression Divide(CompositeExpression other) + { + var result = new CompositeExpression(); + foreach (ExpressionTerm otherTerm in other) + { + result.AddTerms(_terms.Select(x => x.Divide(otherTerm))); + } + + return result; + } + + public static CompositeExpression operator *(CompositeExpression left, CompositeExpression right) + { + return left.Multiply(right); + } + + public static CompositeExpression operator /(CompositeExpression left, CompositeExpression right) + { + return left.Divide(right); + } + + public override string ToString() + { + return string.Join(" + ", _terms); + } + + public CompositeExpression Evaluate(Fraction x) + { + var result = new CompositeExpression(); + result.AddTerms(_terms.Select(t => t.Evaluate(x))); + return result; + } + + public CompositeExpression Evaluate(CompositeExpression expression) + { + var result = new CompositeExpression(); + foreach (ExpressionTerm expressionTerm in _terms) + { + if (expressionTerm.IsConstant) + { + result.Add(expressionTerm); + } + else + { + result.AddTerms(expression.Terms.Select(term => expressionTerm.Evaluate(term))); + } + } + + return result; + } + + #region TermComparer + + private sealed class DescendingOrderTermComparer : IComparer + { + public int Compare(ExpressionTerm? y, ExpressionTerm? x) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + var nestedFunctionComparison = Comparer.Default.Compare(x.NestedFunction, y.NestedFunction); + if (nestedFunctionComparison != 0) return nestedFunctionComparison; + return x.Exponent.Abs().CompareTo(y.Exponent.Abs()); + } + } + + public static IComparer DescendingOrderComparer { get; } = new DescendingOrderTermComparer(); + + #endregion + + #region Implementation of IEnumerable + + public IEnumerator GetEnumerator() + { + return _terms.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CustomFunction.cs b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CustomFunction.cs new file mode 100644 index 0000000000..0ebee1796e --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/CustomFunction.cs @@ -0,0 +1,43 @@ +using System; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Expressions; + +/// +/// A custom function f(ax^n + bx^m) +/// +/// +/// +/// +/// +/// These are functions that we don't directly support, such as Sqrt. +internal record CustomFunction(string Namespace, string Name, CompositeExpression Terms, params string[] AdditionalParameters) : IComparable, IComparable +{ + #region Overrides of Object + + public override string ToString() + { + if(AdditionalParameters.Length == 0) + return $"{Name}({Terms})"; + return $"{Name}({Terms}, {string.Join(", ", AdditionalParameters)})"; + } + + #endregion + + #region Relational members + + public int CompareTo(CustomFunction? other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + return string.Compare(Name, other.Name, StringComparison.Ordinal); + } + + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is CustomFunction other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(CustomFunction)}"); + } + + #endregion +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Expressions/ExpressionTerm.cs b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/ExpressionTerm.cs new file mode 100644 index 0000000000..c836906a73 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Expressions/ExpressionTerm.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Expressions; + +/// +/// A term of the form "a * f(x)^n". +/// +/// The constant coefficient (a) +/// The degree of the term (n) +/// f(x) if one is available +/// When there is no nested function f(x) = x +internal record ExpressionTerm(Fraction Coefficient, Fraction Exponent, CustomFunction? NestedFunction = null) : IComparable, IComparable +{ + public bool IsRational => NestedFunction is null && Exponent.Denominator.IsOne; + + public bool IsConstant => NestedFunction is null && Exponent.IsZero; + + public ExpressionTerm Negate() + { + return this with { Coefficient = Coefficient.Negate() }; + } + + public ExpressionTerm Invert() + { + return this with { Exponent = Exponent.Negate(), Coefficient = Coefficient.Reciprocal() }; + } + + public ExpressionTerm Multiply(ExpressionTerm otherTerm) + { + if (NestedFunction != null && otherTerm.NestedFunction != null && + NestedFunction != otherTerm.NestedFunction) // there aren't any cases of this in the code-base + { + throw new NotSupportedException( + "Multiplying terms with different functions is currently not supported"); // if we need to, we should use a collection or create some function-composition + } + + return new ExpressionTerm(Coefficient * otherTerm.Coefficient, Exponent + otherTerm.Exponent, NestedFunction ?? otherTerm.NestedFunction); + } + + public ExpressionTerm Divide(ExpressionTerm otherTerm) + { + return Multiply(otherTerm.Invert()); + } + + public static ExpressionTerm Constant(Fraction coefficient) + { + return new ExpressionTerm(coefficient, Fraction.Zero); + } + + #region Overrides of Object + + public override string ToString() + { + var coefficientFormat = Coefficient == Fraction.One ? "" : + Coefficient == Fraction.MinusOne ? "-" : $"{Coefficient.ToDouble()} * "; + if (NestedFunction == null) + { + if (Exponent == Fraction.Zero) + { + return $"{Coefficient.ToDouble()}"; + } + + if (Exponent == Fraction.One) + { + return $"{coefficientFormat}x"; + } + + return $"{coefficientFormat}x^{Exponent.ToDouble()}"; + } + + return $"{coefficientFormat}{NestedFunction}"; + } + + #endregion + + public ExpressionTerm Evaluate(Fraction x) + { + if (NestedFunction != null || Exponent.IsZero) + { + return this; + } + + return Constant(Coefficient * x.Pow(Exponent)); + } + + public ExpressionTerm Evaluate(ExpressionTerm term) + { + if (Exponent.IsZero) + { + return this; + } + + if (NestedFunction == null) + { + return new ExpressionTerm(Coefficient * term.Coefficient.Pow(Exponent), term.Exponent * Exponent); + } + + CompositeExpression nestedTerms = NestedFunction.Terms.Evaluate(term); + return this with { NestedFunction = NestedFunction with { Terms = nestedTerms } }; + + } + + #region Relational members + + public int CompareTo(ExpressionTerm? other) + { + if (ReferenceEquals(this, other)) return 0; + if (other is null) return 1; + var exponentComparison = Exponent.CompareTo(other.Exponent); + if (exponentComparison != 0) return exponentComparison; + var nestedFunctionComparison = Comparer.Default.Compare(NestedFunction, other.NestedFunction); + if (nestedFunctionComparison != 0) return nestedFunctionComparison; + return Coefficient.CompareTo(other.Coefficient); + } + + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is ExpressionTerm other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(ExpressionTerm)}"); + } + + #endregion +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/IFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/IFunctionEvaluator.cs new file mode 100644 index 0000000000..fa75d3a0a8 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/IFunctionEvaluator.cs @@ -0,0 +1,26 @@ +using System; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions; + +/// +/// Defines the contract for a function evaluator that can parse and create expressions. +/// +/// +/// Implementations of this interface are used to evaluate specific mathematical functions. +/// +internal interface IFunctionEvaluator +{ + /// + /// Gets the name of the function that this evaluator can handle. + /// + string FunctionName { get; } + + /// + /// Parses the given expression and returns a pending term. + /// + /// The expression to parse. + /// Can be used to evaluate the function body expression. + /// A that represents the parsed expression. + CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, Func expressionResolver); +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/FractionExtensions.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/FractionExtensions.cs new file mode 100644 index 0000000000..e4f8b3a736 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/FractionExtensions.cs @@ -0,0 +1,52 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System.Numerics; +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; + +/// +/// Adds the Pow(Fraction, Fraction) extension method for the Fraction type, which only supports the integer overload: +/// Pow(Fraction, int). +/// +internal static class FractionExtensions +{ + public static readonly Fraction OneHalf = new(BigInteger.One, new BigInteger(2)); + + /// + /// Raises the specified to the power of . + /// + /// The base to be raised to a power. + /// The exponent to which the base is raised. + /// + /// A representing the result of raising to the power of + /// . + /// + /// + /// This method supports integer exponents directly. For fractional exponents, it calculates the result using + /// a double-precision approximation, which may lead to a loss of precision. + /// + public static Fraction Pow(this Fraction x, Fraction power) + { + if (power == Fraction.One) + { + return x; + } + + if (x == Fraction.One) + { + return x; + } + + power = power.Reduce(); + if (power.Denominator.IsOne) + { + return Fraction.Pow(x, (int)power); + } + + // the only way we could reach this line is if we have a fractional power (e.g. square root), or any other custom expression which does not have an inverse. + // there is currently no call that reaches this line, and we should try to keep it this way (avoiding the potential loss of precision) + return Fraction.FromDoubleRounded(System.Math.Pow(x.ToDouble(), power.ToDouble())); + } +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/MathFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/MathFunctionEvaluator.cs new file mode 100644 index 0000000000..ec16d1688a --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/MathFunctionEvaluator.cs @@ -0,0 +1,27 @@ +using System; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; + +internal abstract class MathFunctionEvaluator : IFunctionEvaluator +{ + public virtual string Namespace => nameof(System.Math); + public abstract string FunctionName { get; } + + public CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, Func expressionResolver) + { + CompositeExpression functionBody = expressionResolver(expressionToParse with {Exponent = 1}); + Fraction power = expressionToParse.Exponent; + if (functionBody.IsConstant) // constant expression (directly evaluate the function) + { + var constantTerm = (ExpressionTerm)functionBody; + Fraction resultingValue = Evaluate(constantTerm.Coefficient); + return ExpressionTerm.Constant(resultingValue.Pow(power)); + } + // we cannot expand a function of x + return new ExpressionTerm(1, power, new CustomFunction(Namespace, FunctionName, functionBody)); + } + + public abstract Fraction Evaluate(Fraction value); +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/PowFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/PowFunctionEvaluator.cs new file mode 100644 index 0000000000..5c6b9379df --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/PowFunctionEvaluator.cs @@ -0,0 +1,50 @@ +using System; +using System.Globalization; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; + +internal class PowFunctionEvaluator : IFunctionEvaluator +{ + public string Namespace => nameof(System.Math); + public string FunctionName => nameof(System.Math.Pow); + + public bool ExpandNonConstantExpressions { get; set; } = false; // while it's possible to expand the expression (even for non-rational powers)- probably we shouldn't + + public CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, + Func expressionResolver) + { + var functionParams = expressionToParse.Expression.Split(','); + if (functionParams.Length != 2 || !FractionHelper.TryParseInvariant(functionParams[1], out Fraction exponentParsed)) + { + throw new FormatException($"The provided string is not in the correct format for the Pow function {expressionToParse}"); + } + + CompositeExpression functionBody = expressionResolver(new ExpressionEvaluationTerm(functionParams[0], 1)); + Fraction power = expressionToParse.Exponent * exponentParsed; + + if (functionBody.IsConstant) + { + var singleTerm = (ExpressionTerm)functionBody; + Fraction coefficient = singleTerm.Coefficient.Pow(power); + return ExpressionTerm.Constant(coefficient); + } + + if (!ExpandNonConstantExpressions) + { + return new ExpressionTerm(1, 1, new CustomFunction(Namespace, FunctionName, functionBody, power.ToDecimal().ToString(CultureInfo.InvariantCulture))); + } + + // while it's possible to expand the expression (even for non-rational powers)- we shouldn't, as the operation would not be reversible: the result of (x^0.5)^2 may be different from x + if (functionBody.Terms.Count == 1) + { + var singleTerm = (ExpressionTerm)functionBody; + Fraction coefficient = singleTerm.Coefficient.Pow(power); + return singleTerm with { Coefficient = coefficient, Exponent = singleTerm.Exponent * power }; + } + + // TODO see about handling the multi-term expansion (at least for integer exponents) + return new ExpressionTerm(1, 1, new CustomFunction(Namespace, FunctionName, functionBody, power.ToDecimal().ToString(CultureInfo.InvariantCulture))); + } +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/SqrtFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/SqrtFunctionEvaluator.cs new file mode 100644 index 0000000000..152c459dc2 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/SqrtFunctionEvaluator.cs @@ -0,0 +1,40 @@ +using System; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math; + +internal class SqrtFunctionEvaluator : IFunctionEvaluator +{ + public string Namespace => nameof(System.Math); // we could switch this to QuantityValue if we decide to add it later + public string FunctionName => nameof(System.Math.Sqrt); + public bool ExpandNonConstantExpressions { get; set; } = false; // while it's possible to expand the expression (even for non-rational powers)- probably we shouldn't + + public CompositeExpression CreateExpression(ExpressionEvaluationTerm expressionToParse, Func expressionResolver) + { + CompositeExpression functionBody = expressionResolver(expressionToParse with {Exponent = 1}); + if (functionBody.IsConstant) + { + var constantTerm = (ExpressionTerm)functionBody; + Fraction coefficient = constantTerm.Coefficient.Pow(expressionToParse.Exponent * FractionExtensions.OneHalf); + return ExpressionTerm.Constant(coefficient); + } + + if (!ExpandNonConstantExpressions) + { + return new ExpressionTerm(1, expressionToParse.Exponent, new CustomFunction(Namespace, FunctionName, functionBody)); + } + + // while it's possible to expand the expression (even for non-rational powers)- we shouldn't, as the operation would not be reversible: the result of (x^0.5)^2 may be different from x + Fraction power = expressionToParse.Exponent * FractionExtensions.OneHalf; + if (functionBody.Terms.Count == 1) + { + var constantTerm = (ExpressionTerm)functionBody; + Fraction coefficient = constantTerm.Coefficient.Pow(power); + return constantTerm with { Coefficient = coefficient, Exponent = constantTerm.Exponent * power }; + } + + // TODO see about handling the multi-term expansion (at least for integer exponents) + return new ExpressionTerm(1, power, new CustomFunction(Namespace, FunctionName, functionBody)); + } +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/AsinFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/AsinFunctionEvaluator.cs new file mode 100644 index 0000000000..f2110ac629 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/AsinFunctionEvaluator.cs @@ -0,0 +1,13 @@ +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math.Trigonometry; + +internal class AsinFunctionEvaluator : MathFunctionEvaluator +{ + public override string FunctionName => nameof(System.Math.Asin); + + public override Fraction Evaluate(Fraction value) + { + return Fraction.FromDoubleRounded(System.Math.Asin(value.ToDouble())); + } +} diff --git a/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/SinFunctionEvaluator.cs b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/SinFunctionEvaluator.cs new file mode 100644 index 0000000000..5bab6b1f96 --- /dev/null +++ b/CodeGen/Helpers/ExpressionAnalyzer/Functions/Math/Trigonometry/SinFunctionEvaluator.cs @@ -0,0 +1,13 @@ +using Fractions; + +namespace CodeGen.Helpers.ExpressionAnalyzer.Functions.Math.Trigonometry; + +internal class SinFunctionEvaluator : MathFunctionEvaluator +{ + public override string FunctionName => nameof(System.Math.Sin); + + public override Fraction Evaluate(Fraction value) + { + return Fraction.FromDoubleRounded(System.Math.Sin(value.ToDouble())); + } +} diff --git a/CodeGen/Helpers/ExpressionEvaluationHelpers.cs b/CodeGen/Helpers/ExpressionEvaluationHelpers.cs new file mode 100644 index 0000000000..78b2ad3d36 --- /dev/null +++ b/CodeGen/Helpers/ExpressionEvaluationHelpers.cs @@ -0,0 +1,462 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using CodeGen.Helpers.ExpressionAnalyzer; +using CodeGen.Helpers.ExpressionAnalyzer.Expressions; +using CodeGen.JsonTypes; +using Fractions; + +namespace CodeGen.Helpers; + +internal static class ExpressionEvaluationHelpers +{ + private record struct Factor(BigInteger Number, int Power, BigInteger Value) + { + public static Factor FromNumber(BigInteger number) => new(number, 1, number); + + private sealed class ValueRelationalComparer : IComparer + { + public int Compare(Factor x, Factor y) + { + return x.Value.CompareTo(y.Value); + } + } + + public static IComparer ValueComparer { get; } = new ValueRelationalComparer(); + }; + + private static List ExtractFactors(this BigInteger number) + { + number = BigInteger.Abs(number); + var factors = new List(); + if (number.IsPowerOfTwo) + { + var exponent = (int)(number.GetBitLength() - 1); + var divisor = BigInteger.Pow(2, exponent); + factors.Add(new Factor(2, exponent, divisor)); + return factors; + } + + var factorsToTryFirst = new BigInteger[] {10, 2, 3, 5, 7}; + foreach (BigInteger divisor in factorsToTryFirst) + { + if (TryGetFactors(number, divisor, out number, out Factor factor)) + { + factors.Add(factor); + if (number.IsOne) + { + return factors; + } + + if (number <= long.MaxValue) + { + factors.Add(Factor.FromNumber(number)); + return factors; + } + } + } + + BigInteger currentDivisor = 11; + do + { + if (TryGetFactors(number, currentDivisor, out number, out Factor factor)) + { + factors.Add(factor); + } + + currentDivisor++; + } while (number > long.MaxValue && number > currentDivisor); + + if (!number.IsOne) + { + factors.Add(Factor.FromNumber(number)); + } + + return factors; + } + + private static bool TryGetFactors(BigInteger number, BigInteger divisor, out BigInteger quotient, out Factor factor) + { + quotient = number; + var power = 0; + while (true) + { + var nextQuotient = BigInteger.DivRem(quotient, divisor, out BigInteger remainder); + if (remainder.IsZero) + { + quotient = nextQuotient; + power++; + } + else + { + factor = new Factor(divisor, power, BigInteger.Pow(divisor, power)); + return power > 0; + } + } + } + + private static SortedSet MergeFactors(this IEnumerable factorsToMerge) + { + var factors = new SortedSet(factorsToMerge, Factor.ValueComparer); + while (factors.Count > 1) + { + // try to merge the next two factors + Factor smallestFactor = factors.First(); + Factor secondSmallestFactor = factors.Skip(1).First(); + var mergedFactor = Factor.FromNumber(smallestFactor.Value * secondSmallestFactor.Value); + if (mergedFactor.Value > long.MaxValue) + { + return factors; // we've got the smallest possible set + } + + // replace the two factors with their merged version + factors.Remove(smallestFactor); + factors.Remove(secondSmallestFactor); + factors.Add(mergedFactor); + } + + return factors; + } + + private static string GetConstantFormat(this Factor factor) + { + if (factor.Power == 1) + { + return $"new BigInteger({factor.Value})"; + } + else if (factor.Number == 10) + { + return $"QuantityValue.PowerOfTen({factor.Power})"; + } + else + { + return $"BigInteger.Pow({factor.Number}, {factor.Power})"; + } + } + + private static string GetConstantMultiplicationFormat(this IEnumerable factors, bool negate = false) + { + var expression = string.Join(" * ", factors.Select(x => x.GetConstantFormat())); + if (negate) + { + expression = "-" + expression; + } + + return expression; + } + + private static string GetConstantFormat(this Fraction coefficient) + { + if (coefficient == Fraction.One) + { + return "1"; + } + + return coefficient.Denominator.IsOne + ? coefficient.Numerator.ToString() + : $"new QuantityValue({coefficient.Numerator}, {coefficient.Denominator})"; + } + + private static string GetLongConstantFormat(this Fraction coefficient) + { + var numeratorExpression = coefficient.Numerator > long.MaxValue || coefficient.Numerator < long.MinValue + ? coefficient.Numerator.ExtractFactors().MergeFactors().GetConstantMultiplicationFormat(coefficient.IsNegative) + : coefficient.Numerator.ToString(); + var denominatorExpression = coefficient.Denominator > long.MaxValue + ? coefficient.Denominator.ExtractFactors().MergeFactors().GetConstantMultiplicationFormat() + : coefficient.Denominator.ToString(); + var expandedExpression = $"new QuantityValue({numeratorExpression}, {denominatorExpression})"; + return expandedExpression; + } + + private static string GetFractionalConstantFormat(this Fraction coefficient) + { + coefficient = coefficient.Reduce(); + // making sure that neither the Numerator nor the Denominator contain a value that cannot be represented as a compiler constant + if (coefficient.Numerator >= long.MinValue && coefficient.Numerator <= long.MaxValue && coefficient.Denominator <= long.MaxValue) + { + return coefficient.GetConstantFormat(); + } + + // need to represent the fraction in terms of two terms: "a * b" + return coefficient.GetLongConstantFormat(); + } + + private static string GetConstantExpression(this Fraction coefficient, string csharpParameter) + { + if (coefficient == Fraction.One) + { + return csharpParameter; + } + + if (coefficient.Denominator.IsOne) + { + return $"{csharpParameter} * {coefficient.Numerator}"; + } + + if (coefficient.Numerator.IsOne) + { + return $"{csharpParameter} / {coefficient.Denominator}"; + } + + if (coefficient.Numerator == BigInteger.MinusOne) + { + return $"{csharpParameter} / -{coefficient.Denominator}"; + } + + return $"{csharpParameter} * new QuantityValue({coefficient.Numerator}, {coefficient.Denominator})"; + } + + private static string GetFractionalExpressionFormat(this Fraction coefficient, string csharpParameter) + { + coefficient = coefficient.Reduce(); + // making sure that neither the Numerator nor the Denominator contain a value that cannot be represented as a compiler constant + if (coefficient.Numerator >= long.MinValue && coefficient.Numerator <= long.MaxValue && coefficient.Denominator <= long.MaxValue) + { + return coefficient.GetConstantExpression(csharpParameter); + } + + // need to represent the fraction in terms of two (or more) terms: "x * a * b" + return $"{csharpParameter} * {coefficient.GetLongConstantFormat()}"; + } + + + public static string GetExpressionFormat(this CustomFunction customFunction, string csharpParameter) + { + // TODO see about redirecting these to a static method in the quantity's class which is responsible for handling the required operations (efficiently) + var mainArgument = $"({customFunction.Terms.GetExpressionFormat(csharpParameter)}).ToDouble()"; + var functionArguments = string.Join(", ", customFunction.AdditionalParameters.Prepend(mainArgument)); + return $"QuantityValue.FromDoubleRounded({customFunction.Namespace}.{customFunction.Name}({functionArguments}))"; + } + + public static string GetConstantExpressionFormat(this CustomFunction customFunction) + { + // TODO see about redirecting these to a static method in the quantity's class which is responsible for handling the required operations (efficiently) + var functionArguments = string.Join(", ", customFunction.AdditionalParameters); + return $"QuantityValue.FromDoubleRounded({customFunction.Namespace}.{customFunction.Name}({functionArguments}))"; + } + + public static string GetExponentFormat(this Fraction exponent, string csharpParameter) + { + if (exponent == Fraction.One) + { + return csharpParameter; + } + + // alternatively this could be an operator: e.g. $"({csharpParameter} ^ {exponent.ToInt32()})" + return exponent.Denominator.IsOne + ? $"QuantityValue.Pow({csharpParameter}, {exponent.ToInt32()})" + : $"QuantityValue.FromDoubleRounded(Math.Pow({csharpParameter}.ToDouble(), {exponent.ToDouble()}))"; + } + + public static string GetExpressionFormat(this ExpressionTerm term, string csharpParameter) + { + if (term.IsConstant) + { + return term.Coefficient.GetFractionalConstantFormat(); + } + + if (term is { NestedFunction: not null, Exponent.IsZero: true }) + { + return term.NestedFunction.GetConstantExpressionFormat(); + } + + var expressionFormat = term.NestedFunction is null ? csharpParameter : term.NestedFunction.GetExpressionFormat(csharpParameter); + return term.Coefficient.GetFractionalExpressionFormat(term.Exponent.GetExponentFormat(expressionFormat)); + } + + public static string GetExpressionFormat(this CompositeExpression expression, string csharpParameter) + { + return string.Join(" + ", expression.Terms.Select(x => x.GetExpressionFormat(csharpParameter))); + } + + private static string GetStringExpression(string expression, string csharpParameter, string jsonParameter = "{x}") + { + CompositeExpression compositeExpression = ExpressionEvaluator.Evaluate(expression, jsonParameter); + var expectedFormat = compositeExpression.GetExpressionFormat(csharpParameter); + return expectedFormat; + } + + public static string GetConversionExpressionFormat(this CompositeExpression expression, string csharpParameter = "value") + { + string? coefficientTermFormat = null; + string? exponentFormat = null; + string? customConversionFunctionFormat = null; + string? constantTermValue = null; + + foreach (ExpressionTerm expressionTerm in expression.Terms) + { + if (expressionTerm.IsConstant) + { + constantTermValue = expressionTerm.Coefficient.GetFractionalConstantFormat(); + } + else if (expressionTerm.Exponent == 0) + { + throw new InvalidOperationException("The ConversionExpression class does not support custom functions as the constant term."); + } + else + { + if (coefficientTermFormat is not null || exponentFormat is not null || customConversionFunctionFormat is not null) + { + throw new InvalidOperationException("The ConversionExpression class does not support more than 2 terms"); + } + + coefficientTermFormat = expressionTerm.Coefficient.GetFractionalConstantFormat(); + + if (expressionTerm.NestedFunction is not null) + { + customConversionFunctionFormat = expressionTerm.NestedFunction.GetExpressionFormat(csharpParameter); + } + + if (expressionTerm.Exponent == Fraction.One) + { + continue; + } + + if (expressionTerm.Exponent.Denominator.IsOne) + { + exponentFormat = expressionTerm.Exponent.Numerator.ToString(); + } + else if (customConversionFunctionFormat is null) + { + customConversionFunctionFormat = expressionTerm.Exponent.GetExponentFormat(csharpParameter); + } + else // create a composition between the two functions + { + customConversionFunctionFormat = expressionTerm.Exponent.GetExponentFormat(customConversionFunctionFormat); + } + } + } + + coefficientTermFormat ??= "1"; + + if (constantTermValue is not null && exponentFormat is null && customConversionFunctionFormat is null) + { + return $"new ConversionExpression(coefficient: {coefficientTermFormat}, constantTerm: {constantTermValue})"; + } + + if (customConversionFunctionFormat is not null) + { + return $"new ConversionExpression(coefficient: {coefficientTermFormat}, nestedFunction: {csharpParameter} => {customConversionFunctionFormat}, exponent: {exponentFormat ?? "1"}, constantTerm: {constantTermValue ?? "0"})"; + } + + if (constantTermValue is not null) + { + return $"new ConversionExpression(coefficient: {coefficientTermFormat}, exponent: {exponentFormat ?? "1"}, constantTerm: {constantTermValue})"; + } + + if (exponentFormat is not null) + { + return $"new ConversionExpression(coefficient: {coefficientTermFormat}, exponent: {exponentFormat})"; + } + + // using the implicit constructor from QuantityValue, which is equivalent to "new ConversionExpression({coefficientTermFormat})" + return coefficientTermFormat; + } + + private static string GetConversionExpressionFormat(string expression, string csharpParameter = "value", string jsonParameter = "{x}") + { + CompositeExpression compositeExpression = ExpressionEvaluator.Evaluate(expression, jsonParameter); + var expectedFormat = compositeExpression.GetConversionExpressionFormat(csharpParameter); + return expectedFormat; + } + + /// + /// Gets the format of the conversion from the unit to the base unit. + /// + /// The unit for which to get the conversion format. + /// The C# parameter to be used in the conversion expression. + /// A string representing the format of the conversion from the unit to the base unit. + internal static string GetUnitToBaseConversionFormat(this Unit unit, string csharpParameter = "value") + { + return GetStringExpression(unit.FromUnitToBaseFunc, csharpParameter); + } + + /// + /// Gets the format of the conversion from the base unit to the specified unit. + /// + /// The unit to which the conversion format is to be obtained. + /// The C# parameter to be used in the conversion expression. + /// A string representing the format of the conversion from the base unit to the specified unit. + internal static string GetFromBaseToUnitConversionFormat(this Unit unit, string csharpParameter = "value") + { + return GetStringExpression(unit.FromBaseToUnitFunc, csharpParameter); + } + + /// + /// Gets the format of the conversion from the unit to the base unit using a ConversionExpression. + /// + /// The unit for which to get the conversion format. + /// The C# parameter to be used in the conversion expression. + /// A string representing the constructor of a ConversionExpression from the unit to the base unit. + internal static string GetUnitToBaseConversionExpressionFormat(this Unit unit, string csharpParameter = "value") + { + return GetConversionExpressionFormat(unit.FromUnitToBaseFunc, csharpParameter); + } + + /// + /// Gets the format of the conversion from the base unit to the specified unit using a ConversionExpression. + /// + /// The unit to which the conversion format is to be obtained. + /// The C# parameter to be used in the conversion expression. + /// A string representing the constructor of a ConversionExpression from the base unit to the specified unit. + internal static string GetFromBaseToUnitConversionExpressionFormat(this Unit unit, string csharpParameter = "value") + { + return GetConversionExpressionFormat(unit.FromBaseToUnitFunc, csharpParameter); + } + + /// + /// Generates a dictionary of conversion expressions for a given quantity, mapping each unit to its conversion + /// expressions with other units. + /// + /// The quantity for which conversion expressions are generated. + /// + /// An optional JSON parameter used in the evaluation of conversion expressions. Defaults to + /// "{x}". + /// + /// + /// A dictionary where each key is a unit and the value is another dictionary mapping other units to their + /// respective conversion expressions. + /// + /// + /// Thrown if the calculated conversion expression does not match the expected + /// conversion expression. + /// + internal static Dictionary> GetConversionExpressions(this Quantity quantity, string jsonParameter = "{x}") + { + var conversionsFromBase = new Dictionary(); + var conversionsToBase = new Dictionary(); + Unit baseUnit = quantity.Units.First(unit => unit.SingularName == quantity.BaseUnit); + foreach (Unit unit in quantity.Units) + { + if (unit == baseUnit) continue; + CompositeExpression conversionFromBase = conversionsFromBase[unit] = ExpressionEvaluator.Evaluate(unit.FromBaseToUnitFunc, jsonParameter); + if (conversionFromBase.Terms.Count == 1 && conversionFromBase.Degree.Abs() == Fraction.One) + { + // as long as there aren't any complex functions we can just invert the expression + conversionsToBase[unit] = conversionFromBase.SolveForY(); + } + else + { + // complex conversion functions require an explicit expression in both directions + conversionsToBase[unit] = ExpressionEvaluator.Evaluate(unit.FromUnitToBaseFunc, jsonParameter); + } + } + + var conversionsFrom = new Dictionary> { [baseUnit] = conversionsToBase }; + foreach ((Unit fromUnit, CompositeExpression expressionFromBase) in conversionsFromBase) + { + Dictionary fromUnitConversion = conversionsFrom[fromUnit] = new Dictionary(); + foreach ((Unit otherUnit, CompositeExpression expressionToBase) in conversionsToBase) + { + if (fromUnit == otherUnit) continue; + fromUnitConversion[otherUnit] = expressionFromBase.Evaluate(expressionToBase); + } + + fromUnitConversion[baseUnit] = conversionsFromBase[fromUnit]; + } + + return conversionsFrom; + } +} diff --git a/CodeGen/Helpers/FileInfoExtensions.cs b/CodeGen/Helpers/FileInfoExtensions.cs index a1eeeff9cd..b67cbc895e 100644 --- a/CodeGen/Helpers/FileInfoExtensions.cs +++ b/CodeGen/Helpers/FileInfoExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -15,8 +15,8 @@ public static void EditFile( Dictionary replacements) { var tempFilename = $"{sourceFile.FullName}.edited"; - using (StreamReader input = sourceFile.OpenText()) - using (var output = new StreamWriter(tempFilename)) + using (StreamReader input = CodeGenFile.OpenText(sourceFile.FullName)) + using (var output = CodeGenFile.CreateText(tempFilename)) { while (input.ReadLine() is { } line) { @@ -40,8 +40,6 @@ public static void EditFile( } // Make sure all line endings on Windows are CRLF. - // This is important for opening .nfproj flies in Visual Studio, - // and maybe for some other files too. line = line.Replace("\r", "").Replace("\n", Environment.NewLine); output.WriteLine(line); diff --git a/CodeGen/Helpers/FractionHelper.cs b/CodeGen/Helpers/FractionHelper.cs new file mode 100644 index 0000000000..0573c5b436 --- /dev/null +++ b/CodeGen/Helpers/FractionHelper.cs @@ -0,0 +1,37 @@ +using System.Globalization; +using Fractions; + +namespace CodeGen.Helpers; + +/// +/// Helper methods for parsing Fraction values with culture-invariant formatting. +/// +internal static class FractionHelper +{ + /// + /// Attempts to parse a string representation of a number into a normalized Fraction, using invariant culture.
+ /// It supports various number formats including integers, floating point, and scientific notation. + ///
+ /// The string representation of the number to parse. + /// When this method returns, contains the Fraction equivalent of the number contained in value, if the conversion succeeded, or default if the conversion failed. + /// true if value was converted successfully; otherwise, false. + /// + /// The fraction is normalized, e.g. "2/4" is reduced to "1/2". + ///
+ /// This method uses NumberStyles.Number | NumberStyles.Float to support: + /// - Integers: "42", "-123" + /// - Floating point: "3.14", "-2.5" + /// - Scientific notation: "1.5e3", "2.5E-4" + /// - Leading/trailing whitespace + /// + /// All parsing uses InvariantCulture to ensure consistent behavior regardless of system culture settings. + ///
+ public static bool TryParseInvariant(string value, out Fraction result) + { + return Fraction.TryParse( + value, + NumberStyles.Number | NumberStyles.Float, + CultureInfo.InvariantCulture, + out result); + } +} diff --git a/CodeGen/Helpers/HashGuid.cs b/CodeGen/Helpers/HashGuid.cs index 841af59878..7e8271e02f 100644 --- a/CodeGen/Helpers/HashGuid.cs +++ b/CodeGen/Helpers/HashGuid.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; diff --git a/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefix.cs b/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefix.cs index c73ee0106b..56c63d7fe0 100644 --- a/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefix.cs +++ b/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefix.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using CodeGen.JsonTypes; diff --git a/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefixes.cs b/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefixes.cs index 2822de4fc0..fb908be08f 100644 --- a/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefixes.cs +++ b/CodeGen/Helpers/PrefixBuilder/BaseUnitPrefixes.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System.Collections.Generic; diff --git a/CodeGen/Helpers/PrefixBuilder/PrefixBuilderExtensions.cs b/CodeGen/Helpers/PrefixBuilder/PrefixBuilderExtensions.cs index a39c526ffa..955dadf546 100644 --- a/CodeGen/Helpers/PrefixBuilder/PrefixBuilderExtensions.cs +++ b/CodeGen/Helpers/PrefixBuilder/PrefixBuilderExtensions.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index 7ef1a63139..bea51f59d4 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -1,10 +1,12 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.RegularExpressions; +using CodeGen.Exceptions; using CodeGen.JsonTypes; namespace CodeGen.Helpers.PrefixBuilder; @@ -18,6 +20,21 @@ namespace CodeGen.Helpers.PrefixBuilder; /// internal class UnitPrefixBuilder { + /// + /// Matches abbreviations whose leading unit token is raised to the second, third, or fourth power using a + /// superscript or caret, such as , ft³/s, or m^4. + /// + /// + /// This is intentionally not a general unit-expression parser. It does not match a power in a denominator or later + /// compound term (kg/m³, m/s², or N·m²), a leading numeric scale factor + /// (10³·m³), parenthesized or implicit powers, or powers other than two through four. These cases do not put + /// a metric prefix directly before an explicitly powered leading unit token, or are outside the scope of this + /// heuristic. Unit names starting with Square or Cubic are checked separately. + /// + private static readonly Regex LeadingPoweredUnitAbbreviationRegex = new( + @"^[^\s/·*()\-\d]+(?:[²³⁴]|\^[234])", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private readonly BaseUnitPrefixes _prefixes; /// @@ -45,6 +62,9 @@ public UnitPrefixBuilder(BaseUnitPrefixes prefixes) /// /// Thrown when an error occurs while processing a prefix for a unit, such as an invalid prefix or unit configuration. /// + /// + /// Thrown when prefixes are configured for a unit that looks like a powered unit. + /// /// /// This method iterates through the existing units of the specified and applies each defined /// prefix to generate new prefixed units. It ensures that the singular and plural names, conversion functions, @@ -54,34 +74,86 @@ public List GeneratePrefixUnits(Quantity quantity) { var unitsToAdd = new List(); foreach (Unit unit in quantity.Units) - foreach (Prefix prefix in unit.Prefixes) { - try + if (!unit.Prefixes.Any()) { - PrefixInfo prefixInfo = PrefixInfo.Entries[prefix]; - - unitsToAdd.Add(new Unit - { - SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter" - PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter" - BaseUnits = GetPrefixedBaseUnits(quantity.BaseDimensions, unit.BaseUnits, prefixInfo), - FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}", - FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}", - Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo), - ObsoleteText = unit.ObsoleteText, - SkipConversionGeneration = unit.SkipConversionGeneration, - AllowAbbreviationLookup = unit.AllowAbbreviationLookup - }); + continue; } - catch (Exception e) + + ThrowIfPrefixesAreUnsafeForPoweredUnit(quantity, unit); + + foreach (Prefix prefix in unit.Prefixes) { - throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e); + try + { + PrefixInfo prefixInfo = PrefixInfo.Entries[prefix]; + + unitsToAdd.Add(new Unit + { + SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter" + PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter" + BaseUnits = GetPrefixedBaseUnits(quantity.BaseDimensions, unit.BaseUnits, prefixInfo), + FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}", + FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}", + Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo), + ObsoleteText = unit.ObsoleteText, + SkipConversionGeneration = unit.SkipConversionGeneration, + AllowAbbreviationLookup = unit.AllowAbbreviationLookup + }); + } + catch (Exception e) + { + throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e); + } } } return unitsToAdd; } + /// + /// Prevents automatic prefixes from being applied to units that appear to represent a directly powered unit. + /// + /// The quantity that defines the unit. + /// The unit configured with one or more automatic prefixes. + /// + /// Thrown when the unit name starts with Square or Cubic, or when one of its abbreviations starts with + /// a unit token raised to the second, third, or fourth power. + /// + /// + /// Mechanically prefixing a powered unit can produce a misleading abbreviation. For example, prefixing + /// CubicMeter with Kilo produces km³, which means cubic kilometer rather than one thousand + /// cubic meters. This guard uses unit names and as a focused + /// heuristic. It intentionally does not inspect base dimensions, since valid derived units such as watt and joule + /// have powered dimensions but can safely use automatic prefixes. + /// + private static void ThrowIfPrefixesAreUnsafeForPoweredUnit(Quantity quantity, Unit unit) + { + if (!LooksLikePoweredUnit(unit)) + { + return; + } + + throw new UnitsNetCodeGenException( + $"Prefixes cannot be used on {quantity.Name}.{unit.SingularName} because it looks like a powered unit. " + + "Define explicit units instead, such as CubicKilometer for km³ or ThousandCubicMeter for 1000 m³."); + } + + private static bool LooksLikePoweredUnit(Unit unit) + { + // This intentionally checks naming conventions rather than dimensions, since derived units such as Watt, Joule, + // and Ohm have powered base dimensions and safely support prefixes. + if (unit.SingularName.StartsWith("Square", StringComparison.Ordinal) || + unit.SingularName.StartsWith("Cubic", StringComparison.Ordinal)) + { + return true; + } + + return unit.Localization + .SelectMany(localization => localization.Abbreviations) + .Any(abbreviation => LeadingPoweredUnitAbbreviationRegex.IsMatch(abbreviation)); + } + /// /// Applies a metric prefix to the specified base units based on the given dimensions and prefix information. /// diff --git a/CodeGen/Helpers/UnitEnumValueAllocation/QuantityNameToUnitEnumValues.cs b/CodeGen/Helpers/UnitEnumValueAllocation/QuantityNameToUnitEnumValues.cs index bc5071ebdd..10f84ccb09 100644 --- a/CodeGen/Helpers/UnitEnumValueAllocation/QuantityNameToUnitEnumValues.cs +++ b/CodeGen/Helpers/UnitEnumValueAllocation/QuantityNameToUnitEnumValues.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System.Collections.Generic; diff --git a/CodeGen/Helpers/UnitEnumValueAllocation/UnitEnumValueAllocator.cs b/CodeGen/Helpers/UnitEnumValueAllocation/UnitEnumValueAllocator.cs index 5fd3f51ef1..e8be991564 100644 --- a/CodeGen/Helpers/UnitEnumValueAllocation/UnitEnumValueAllocator.cs +++ b/CodeGen/Helpers/UnitEnumValueAllocation/UnitEnumValueAllocator.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; @@ -9,6 +9,7 @@ using System.Text.Json; using CodeGen.Exceptions; using CodeGen.JsonTypes; +using CodeGen.Helpers; using Serilog; namespace CodeGen.Helpers.UnitEnumValueAllocation @@ -150,7 +151,7 @@ private void SaveToFile() "); fileContentStringBuilder.AppendLine(JsonSerializer.Serialize(_quantityNameToUnitEnumValues, JsonOptions)); - File.WriteAllText(_jsonFile, fileContentStringBuilder.ToString()); + CodeGenFile.WriteAllText(_jsonFile, fileContentStringBuilder.ToString()); } /// @@ -162,7 +163,7 @@ private static QuantityNameToUnitEnumValues ReadFromFile(string jsonFile) { if (File.Exists(jsonFile)) { - return JsonSerializer.Deserialize(File.ReadAllText(jsonFile), JsonOptions) + return JsonSerializer.Deserialize(CodeGenFile.ReadAllText(jsonFile), JsonOptions) ?? throw new InvalidOperationException($"Failed to deserialize file: {jsonFile}"); } diff --git a/CodeGen/JsonTypes/BaseDimensions.cs b/CodeGen/JsonTypes/BaseDimensions.cs index 59f9389505..91b4df07a3 100644 --- a/CodeGen/JsonTypes/BaseDimensions.cs +++ b/CodeGen/JsonTypes/BaseDimensions.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System.Text; @@ -54,10 +54,10 @@ private static void AppendDimensionString(StringBuilder sb, string name, int val case 0: return; case 1: - sb.AppendFormat("[{0}]", name); + sb.Append(name); break; default: - sb.AppendFormat("[{0}^{1}]", name, value); + sb.Append($"{name}^{value}"); break; } } diff --git a/CodeGen/JsonTypes/Quantity.cs b/CodeGen/JsonTypes/Quantity.cs index 3ac77e59ce..f416859912 100644 --- a/CodeGen/JsonTypes/Quantity.cs +++ b/CodeGen/JsonTypes/Quantity.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; diff --git a/CodeGen/JsonTypes/QuantityRelation.cs b/CodeGen/JsonTypes/QuantityRelation.cs index 35e97a6bc0..34e6711ac5 100644 --- a/CodeGen/JsonTypes/QuantityRelation.cs +++ b/CodeGen/JsonTypes/QuantityRelation.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; diff --git a/CodeGen/JsonTypes/Unit.cs b/CodeGen/JsonTypes/Unit.cs index 982dd5a887..aef1127ee6 100644 --- a/CodeGen/JsonTypes/Unit.cs +++ b/CodeGen/JsonTypes/Unit.cs @@ -1,4 +1,4 @@ -// Licensed under MIT No Attribution, see LICENSE file at the root. +// Licensed under MIT No Attribution, see LICENSE file at the root. // Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. using System; diff --git a/CodeGen/PrefixInfo.cs b/CodeGen/PrefixInfo.cs index e44c643c48..f63dc03ceb 100644 --- a/CodeGen/PrefixInfo.cs +++ b/CodeGen/PrefixInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using CodeGen.JsonTypes; diff --git a/CodeGen/Program.cs b/CodeGen/Program.cs index 31038895de..b82d3caa60 100644 --- a/CodeGen/Program.cs +++ b/CodeGen/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Linq; @@ -43,9 +43,7 @@ public class Program /// /// Verbose output? Defaults to false. /// The repository root directory, defaults to searching parent directories for UnitsNet.sln. - /// Skip generate nanoFramework Units? Defaults to false - /// Update nanoFramework nuget dependencies? Defaults to false. - public static int Main(bool verbose = false, DirectoryInfo? repositoryRoot = null, bool skipNanoFramework = false, bool updateNanoFrameworkDependencies = false) + public static int Main(bool verbose = false, DirectoryInfo? repositoryRoot = null) { Log.Logger = new LoggerConfiguration() .WriteTo @@ -73,22 +71,6 @@ public static int Main(bool verbose = false, DirectoryInfo? repositoryRoot = nul UnitsNetGenerator.Generate(rootDir, quantities, quantityNameToUnitEnumValues); - if (updateNanoFrameworkDependencies) - { - if (!NanoFrameworkGenerator.UpdateNanoFrameworkDependencies( - rootDir, - quantities)) - { - return 1; - } - } - - if (!skipNanoFramework) - { - Log.Information("Generate nanoFramework projects\n---"); - NanoFrameworkGenerator.Generate(rootDir, quantities, quantityNameToUnitEnumValues); - } - Log.Information("Completed in {ElapsedMs} ms!", sw.ElapsedMilliseconds); return 0; } diff --git a/CodeGen/README.md b/CodeGen/README.md index ae011f55af..2e25af4925 100644 --- a/CodeGen/README.md +++ b/CodeGen/README.md @@ -34,11 +34,3 @@ CodeGen.exe --ver Hit TAB and it should now suggest `--version` and `--verbose` parameters. This should work with any .exe that is compiled with Dragonfruit's app model. - -## nanoFramework - -.NET [nanoFramework](https://github.com/nanoframework/Home) goal is to be a platform that enables the writing of managed code applications for constrained embedded devices. Developers can harness the familiar IDE Visual Studio and their .NET (C#) knowledge to quickly write applications without having to worry about the low level hardware intricacies of a micro-controller. Examples of supported micro controllers are ESP32, STM32F429I, ST Nuclea64, TI CC3220SF and more! The pleasure of .NET where everyone is usually writing in C/C++. - -By default, the code generator will as well generate automatically 1 project per Quantity. - -You can load all the projects at once using the automatically generated solution ```UnitsNet.nanoFramework.sln``` diff --git a/Common/UnitDefinitions/AreaDensity.json b/Common/UnitDefinitions/AreaDensity.json index 4e66410de6..9c70feda53 100644 --- a/Common/UnitDefinitions/AreaDensity.json +++ b/Common/UnitDefinitions/AreaDensity.json @@ -56,6 +56,38 @@ "Abbreviations": [ "mg/m²" ] } ] + }, + { + "SingularName": "PoundPerSquareFoot", + "PluralName": "PoundsPerSquareFoot", + "BaseUnits": { + "L": "Foot", + "M": "Pound" + }, + "FromUnitToBaseFunc": "{x} * (0.45359237 / 0.09290304)", + "FromBaseToUnitFunc": "{x} / (0.45359237 / 0.09290304)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "lb/ft²", "lbs/ft²", "lb/SF", "lbs/SF" ] + } + ] + }, + { + "SingularName": "PoundPerThousandSquareFeet", + "PluralName": "PoundsPerThousandSquareFeet", + "BaseUnits": { + "L": "Foot", + "M": "Pound" + }, + "FromUnitToBaseFunc": "{x} * (0.45359237 / 0.09290304) / 1000", + "FromBaseToUnitFunc": "{x} / (0.45359237 / 0.09290304) * 1000", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "lb/MSF", "lbs/MSF", "lb/1000ft²", "lbs/1000ft²", "lbm/MSF" ] + } + ] } ] } diff --git a/Common/UnitDefinitions/AreaPerLength.json b/Common/UnitDefinitions/AreaPerLength.json new file mode 100644 index 0000000000..25960b59c6 --- /dev/null +++ b/Common/UnitDefinitions/AreaPerLength.json @@ -0,0 +1,85 @@ +{ + "Name": "AreaPerLength", + "BaseUnit": "SquareMeterPerMeter", + "XmlDocSummary": "The magnitude of area per unit length, typically used in structural engineering to specify distributed reinforcement.", + "BaseDimensions": { + "L": 1 + }, + "Units": [ + { + "SingularName": "SquareMeterPerMeter", + "PluralName": "SquareMetersPerMeter", + "BaseUnits": { + "L": "Meter" + }, + "FromUnitToBaseFunc": "{x}", + "FromBaseToUnitFunc": "{x}", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "m²/m" ] + } + ] + }, + { + "SingularName": "SquareCentimeterPerMeter", + "PluralName": "SquareCentimetersPerMeter", + "FromUnitToBaseFunc": "{x} * 1e-4", + "FromBaseToUnitFunc": "{x} / 1e-4", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "cm²/m" ] + } + ] + }, + { + "SingularName": "SquareMillimeterPerMeter", + "PluralName": "SquareMillimetersPerMeter", + "FromUnitToBaseFunc": "{x} * 1e-6", + "FromBaseToUnitFunc": "{x} / 1e-6", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "mm²/m" ] + } + ] + }, + { + "SingularName": "SquareInchPerFoot", + "PluralName": "SquareInchesPerFoot", + "FromUnitToBaseFunc": "{x} * 0.00064516 / 0.3048", + "FromBaseToUnitFunc": "{x} * 0.3048 / 0.00064516", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "in²/ft" ] + } + ] + }, + { + "SingularName": "SquareInchPerInch", + "PluralName": "SquareInchesPerInch", + "FromUnitToBaseFunc": "{x} * 0.00064516 / 0.0254", + "FromBaseToUnitFunc": "{x} * 0.0254 / 0.00064516", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "in²/in" ] + } + ] + }, + { + "SingularName": "SquareFootPerFoot", + "PluralName": "SquareFeetPerFoot", + "FromUnitToBaseFunc": "{x} * 0.09290304 / 0.3048", + "FromBaseToUnitFunc": "{x} * 0.3048 / 0.09290304", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "ft²/ft" ] + } + ] + } + ] +} \ No newline at end of file diff --git a/Common/UnitDefinitions/Compressibility.json b/Common/UnitDefinitions/Compressibility.json index 7d3b14cb30..d8c28fae95 100644 --- a/Common/UnitDefinitions/Compressibility.json +++ b/Common/UnitDefinitions/Compressibility.json @@ -28,8 +28,8 @@ { "SingularName": "InverseKilopascal", "PluralName": "InverseKilopascals", - "FromUnitToBaseFunc": "{x} * 1e3", - "FromBaseToUnitFunc": "{x} / 1e3", + "FromUnitToBaseFunc": "{x} / 1e3", + "FromBaseToUnitFunc": "{x} * 1e3", "Localization": [ { "Culture": "en-US", @@ -40,8 +40,8 @@ { "SingularName": "InverseMegapascal", "PluralName": "InverseMegapascals", - "FromUnitToBaseFunc": "{x} * 1e6", - "FromBaseToUnitFunc": "{x} / 1e6", + "FromUnitToBaseFunc": "{x} / 1e6", + "FromBaseToUnitFunc": "{x} * 1e6", "Localization": [ { "Culture": "en-US", @@ -52,8 +52,8 @@ { "SingularName": "InverseAtmosphere", "PluralName": "InverseAtmospheres", - "FromUnitToBaseFunc": "{x} * 101325", - "FromBaseToUnitFunc": "{x} / 101325", + "FromUnitToBaseFunc": "{x} / 101325", + "FromBaseToUnitFunc": "{x} * 101325", "Localization": [ { "Culture": "en-US", @@ -64,8 +64,8 @@ { "SingularName": "InverseMillibar", "PluralName": "InverseMillibars", - "FromUnitToBaseFunc": "{x} * 100", - "FromBaseToUnitFunc": "{x} / 100", + "FromUnitToBaseFunc": "{x} / 100", + "FromBaseToUnitFunc": "{x} * 100", "Localization": [ { "Culture": "en-US", @@ -76,8 +76,8 @@ { "SingularName": "InverseBar", "PluralName": "InverseBars", - "FromUnitToBaseFunc": "{x} * 1e5", - "FromBaseToUnitFunc": "{x} / 1e5", + "FromUnitToBaseFunc": "{x} / 1e5", + "FromBaseToUnitFunc": "{x} * 1e5", "Localization": [ { "Culture": "en-US", @@ -88,8 +88,8 @@ { "SingularName": "InversePoundForcePerSquareInch", "PluralName": "InversePoundsForcePerSquareInch", - "FromUnitToBaseFunc": "{x} * 6.894757293168361e3", - "FromBaseToUnitFunc": "{x} / 6.894757293168361e3", + "FromUnitToBaseFunc": "{x} / 6.894757293168361e3", + "FromBaseToUnitFunc": "{x} * 6.894757293168361e3", "Localization": [ { "Culture": "en-US", diff --git a/Common/UnitDefinitions/ElectricCurrentGradient.json b/Common/UnitDefinitions/ElectricCurrentGradient.json index 28476d2d0d..32203357e4 100644 --- a/Common/UnitDefinitions/ElectricCurrentGradient.json +++ b/Common/UnitDefinitions/ElectricCurrentGradient.json @@ -1,4 +1,4 @@ -{ +{ "Name": "ElectricCurrentGradient", "BaseUnit": "AmperePerSecond", "XmlDocSummary": "In electromagnetism, the current gradient describes how the current changes in time.", diff --git a/Common/UnitDefinitions/FluidResistance.json b/Common/UnitDefinitions/FluidResistance.json index 6b90951bdc..144d7decee 100644 --- a/Common/UnitDefinitions/FluidResistance.json +++ b/Common/UnitDefinitions/FluidResistance.json @@ -1,4 +1,4 @@ -{ +{ "Name": "FluidResistance", "BaseUnit": "PascalSecondPerCubicMeter", "XmlDocSummary": "Fluid Resistance is a force acting opposite to the relative motion of any object moving with respect to a surrounding fluid. Fluid Resistance is sometimes referred to as drag or fluid friction.", diff --git a/Common/UnitDefinitions/Force.json b/Common/UnitDefinitions/Force.json index 9e1d26292a..88a0ddd929 100644 --- a/Common/UnitDefinitions/Force.json +++ b/Common/UnitDefinitions/Force.json @@ -31,6 +31,24 @@ } ] }, + { + "SingularName": "GramForce", + "PluralName": "GramsForce", + "FromUnitToBaseFunc": "{x} * 9.80665e-3", + "FromBaseToUnitFunc": "{x} / 9.80665e-3", + "XmlDocSummary": "The gram-force is a unit of force equal to the magnitude of force exerted by a gram of mass in standard gravity (9.80665 m/s²). It is equal to 9.80665 × 10⁻³ N.", + "XmlDocRemarks": "https://en.wikipedia.org/wiki/Kilogram-force", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "gf" ] + }, + { + "Culture": "ru-RU", + "Abbreviations": [ "гс" ] + } + ] + }, { "SingularName": "KilogramForce", "PluralName": "KilogramsForce", diff --git a/Common/UnitDefinitions/HeatFlux.json b/Common/UnitDefinitions/HeatFlux.json index 9d3204a3dd..3c4665a6b3 100644 --- a/Common/UnitDefinitions/HeatFlux.json +++ b/Common/UnitDefinitions/HeatFlux.json @@ -24,6 +24,23 @@ } ] }, + { + "SingularName": "WattPerSquareMillimeter", + "PluralName": "WattsPerSquareMillimeter", + "BaseUnits": { + "M": "Kilogram", + "T": "Second" + }, + "FromUnitToBaseFunc": "{x} / 1e-6", + "FromBaseToUnitFunc": "{x} * 1e-6", + "Prefixes": [ "Nano", "Micro", "Milli", "Centi", "Deci", "Kilo" ], + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "W/mm²" ] + } + ] + }, { "SingularName": "WattPerSquareInch", "PluralName": "WattsPerSquareInch", diff --git a/Common/UnitDefinitions/HeatTransferCoefficient.json b/Common/UnitDefinitions/HeatTransferCoefficient.json index f3108375f2..47dd1b4546 100644 --- a/Common/UnitDefinitions/HeatTransferCoefficient.json +++ b/Common/UnitDefinitions/HeatTransferCoefficient.json @@ -54,6 +54,18 @@ } ] }, + { + "SingularName": "BtuPerSecondSquareInchDegreeFahrenheit", + "PluralName": "BtusPerSecondSquareInchDegreeFahrenheit", + "FromUnitToBaseFunc": "{x} * ((1055.05585262 / (2.54e-2 * 2.54e-2)) * 1.8)", + "FromBaseToUnitFunc": "{x} / ((1055.05585262 / (2.54e-2 * 2.54e-2)) * 1.8)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "Btu/(s·in²·°F)", "Btu/(in²·s·°F)", "Btu/(s*in^2*degF)", "Btu/(in^2*s*degF)" ] + } + ] + }, { "SingularName": "CaloriePerHourSquareMeterDegreeCelsius", "PluralName": "CaloriesPerHourSquareMeterDegreeCelsius", diff --git a/Common/UnitDefinitions/MolarFlow.json b/Common/UnitDefinitions/MolarFlow.json index e9d90d51e4..af75991ed2 100644 --- a/Common/UnitDefinitions/MolarFlow.json +++ b/Common/UnitDefinitions/MolarFlow.json @@ -54,7 +54,7 @@ "Localization": [ { "Culture": "en-US", - "Abbreviations": [ "kmol/h" ] + "Abbreviations": [ "mol/h" ] } ] }, diff --git a/Common/UnitDefinitions/MolarMass.json b/Common/UnitDefinitions/MolarMass.json index edcda34e72..770da97f19 100644 --- a/Common/UnitDefinitions/MolarMass.json +++ b/Common/UnitDefinitions/MolarMass.json @@ -47,6 +47,7 @@ { "SingularName": "PoundPerMole", "PluralName": "PoundsPerMole", + "XmlDocSummary": "Pound mass per SI mole. This is distinct from PoundPerPoundMole, where the denominator is the pound-mole amount of substance unit.", "BaseUnits": { "M": "Pound", "N": "Mole" @@ -64,6 +65,24 @@ "Abbreviations": [ "фунт/моль" ] } ] + }, + { + "SingularName": "PoundPerPoundMole", + "PluralName": "PoundsPerPoundMole", + "XmlDocSummary": "Pound mass per pound-mole. This is numerically equal to kilograms per kilomole and distinct from PoundPerMole, where the denominator is the SI mole.", + "XmlDocRemarks": "Sources: https://www.engineeringtoolbox.com/molecular-weight-gas-vapor-d_1156.html, https://www.engineeringtoolbox.com/unit-converter-d_185.html", + "BaseUnits": { + "M": "Pound", + "N": "PoundMole" + }, + "FromUnitToBaseFunc": "{x} / 1e3", + "FromBaseToUnitFunc": "{x} * 1e3", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "lb/lbmol", "lbm/lbmol" ] + } + ] } ] } diff --git a/Common/UnitDefinitions/PorousMediumPermeability.json b/Common/UnitDefinitions/PorousMediumPermeability.json index 746e8805ab..2e5e62fe60 100644 --- a/Common/UnitDefinitions/PorousMediumPermeability.json +++ b/Common/UnitDefinitions/PorousMediumPermeability.json @@ -1,4 +1,4 @@ -{ +{ "Name": "PorousMediumPermeability", "BaseUnit": "SquareMeter", "XmlDoc": "In fluid mechanics, permeability is the measure of the ability of a porous material to allow fluids to pass through it.", diff --git a/Common/UnitDefinitions/PowerDensity.json b/Common/UnitDefinitions/PowerDensity.json index 8a4d3fa962..33073faa6f 100644 --- a/Common/UnitDefinitions/PowerDensity.json +++ b/Common/UnitDefinitions/PowerDensity.json @@ -64,6 +64,32 @@ "Abbreviations": [ "W/l" ] } ] + }, + { + "SingularName": "BtuPerSecondCubicInch", + "PluralName": "BtusPerSecondCubicInch", + "XmlDocRemarks": "Based on the International Table (IT) definition of the British thermal unit (BTU), where 1 BTU is defined as exactly 1055.05585262 joules (≈1.05506 kJ). See https://en.wikipedia.org/wiki/British_thermal_unit for details.", + "FromUnitToBaseFunc": "{x} * 1055.05585262 / (2.54e-2 * 2.54e-2 * 2.54e-2)", + "FromBaseToUnitFunc": "{x} / 1055.05585262 * (2.54e-2 * 2.54e-2 * 2.54e-2)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "BTU/(s·in³)" ] + } + ] + }, + { + "SingularName": "BtuPerSecondCubicFoot", + "PluralName": "BtusPerSecondCubicFoot", + "XmlDocRemarks": "Based on the International Table (IT) definition of the British thermal unit (BTU), where 1 BTU is defined as exactly 1055.05585262 joules (≈1.05506 kJ). See https://en.wikipedia.org/wiki/British_thermal_unit for details.", + "FromUnitToBaseFunc": "{x} * 1055.05585262 / (0.3048 * 0.3048 * 0.3048)", + "FromBaseToUnitFunc": "{x} / 1055.05585262 * (0.3048 * 0.3048 * 0.3048)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "BTU/(s·ft³)" ] + } + ] } ] } diff --git a/Common/UnitDefinitions/Pressure.json b/Common/UnitDefinitions/Pressure.json index c5411d1cb9..1bfaf11cea 100644 --- a/Common/UnitDefinitions/Pressure.json +++ b/Common/UnitDefinitions/Pressure.json @@ -401,6 +401,34 @@ "Abbreviations": [ "inH2O", "inch wc", "wc" ] } ] + }, + { + "SingularName": "MilligramForcePerSquareMeter", + "PluralName": "MilligramsForcePerSquareMeter", + "FromUnitToBaseFunc": "{x} * 9.80665e-6", + "FromBaseToUnitFunc": "{x} / 9.80665e-6", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "mgf/m²" ] + }, + { + "Culture": "ru-RU", + "Abbreviations": [ "мгс/м²" ] + } + ] + }, + { + "SingularName": "MilligramForcePerSquareFoot", + "PluralName": "MilligramsForcePerSquareFoot", + "FromUnitToBaseFunc": "{x} * 9.80665e-6 / 9.290304e-2", + "FromBaseToUnitFunc": "{x} / 9.80665e-6 * 9.290304e-2", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "mgf/ft²" ] + } + ] } ] } diff --git a/Common/UnitDefinitions/RadiationEquivalentDose.json b/Common/UnitDefinitions/RadiationEquivalentDose.json index 85da7bbed3..325601bdc3 100644 --- a/Common/UnitDefinitions/RadiationEquivalentDose.json +++ b/Common/UnitDefinitions/RadiationEquivalentDose.json @@ -1,4 +1,4 @@ -{ +{ "Name": "RadiationEquivalentDose", "BaseUnit": "Sievert", "XmlDocSummary": "Equivalent dose is a dose quantity representing the stochastic health effects of low levels of ionizing radiation on the human body which represents the probability of radiation-induced cancer and genetic damage.", diff --git a/Common/UnitDefinitions/RadiationExposure.json b/Common/UnitDefinitions/RadiationExposure.json index 6684939d72..7385f919f3 100644 --- a/Common/UnitDefinitions/RadiationExposure.json +++ b/Common/UnitDefinitions/RadiationExposure.json @@ -1,4 +1,4 @@ -{ +{ "Name": "RadiationExposure", "BaseUnit": "CoulombPerKilogram", "XmlDocSummary": "Radiation exposure is a measure of the ionization of air due to ionizing radiation from photons.", diff --git a/Common/UnitDefinitions/Radioactivity.json b/Common/UnitDefinitions/Radioactivity.json index 56ccccdef9..08af830748 100644 --- a/Common/UnitDefinitions/Radioactivity.json +++ b/Common/UnitDefinitions/Radioactivity.json @@ -1,4 +1,4 @@ -{ +{ "Name": "Radioactivity", "BaseUnit": "Becquerel", "XmlDocSummary": "Amount of ionizing radiation released when an element spontaneously emits energy as a result of the radioactive decay of an unstable atom per unit time.", diff --git a/Common/UnitDefinitions/SpecificVolume.json b/Common/UnitDefinitions/SpecificVolume.json index aa9939832a..164c7505ce 100644 --- a/Common/UnitDefinitions/SpecificVolume.json +++ b/Common/UnitDefinitions/SpecificVolume.json @@ -16,7 +16,6 @@ }, "FromUnitToBaseFunc": "{x}", "FromBaseToUnitFunc": "{x}", - "Prefixes": [ "Milli" ], "Localization": [ { "Culture": "en-US", @@ -24,6 +23,24 @@ } ] }, + { + "SingularName": "CubicMillimeterPerKilogram", + "PluralName": "CubicMillimetersPerKilogram", + "BaseUnits": { + "L": "Millimeter", + "M": "Kilogram" + }, + "XmlDocSummary": "A specific volume unit of cubic millimeters per kilogram. The millimeter prefix applies before cubing the length unit, so 1 mm³/kg = 1e-9 m³/kg.", + "XmlDocRemarks": "Sources: https://www.bipm.org/en/measurement-units/si-prefixes, https://usma.org/wp-content/uploads/2015/06/Practical_Guide_to_the_SI.pdf", + "FromUnitToBaseFunc": "{x} / 1e9", + "FromBaseToUnitFunc": "{x} * 1e9", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "mm³/kg" ] + } + ] + }, { "SingularName": "CubicFootPerPound", "PluralName": "CubicFeetPerPound", diff --git a/Common/UnitDefinitions/ThermalConductivity.json b/Common/UnitDefinitions/ThermalConductivity.json index c5f30282b8..284fba5818 100644 --- a/Common/UnitDefinitions/ThermalConductivity.json +++ b/Common/UnitDefinitions/ThermalConductivity.json @@ -39,6 +39,18 @@ "Abbreviations": [ "BTU/(h·ft·°F)" ] } ] + }, + { + "SingularName": "BtuPerSecondInchFahrenheit", + "PluralName": "BtusPerSecondInchFahrenheit", + "FromUnitToBaseFunc": "{x} * ((1055.05585262 / 2.54e-2) * 1.8)", + "FromBaseToUnitFunc": "{x} / ((1055.05585262 / 2.54e-2) * 1.8)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "BTU/(s·in·°F)" ] + } + ] } ] } diff --git a/Common/UnitDefinitions/Torque.json b/Common/UnitDefinitions/Torque.json index d19cebd6ad..d9078ae1a3 100644 --- a/Common/UnitDefinitions/Torque.json +++ b/Common/UnitDefinitions/Torque.json @@ -56,6 +56,30 @@ } ] }, + { + "SingularName": "OunceForceFoot", + "PluralName": "OunceForceFeet", + "FromUnitToBaseFunc": "{x} * (4.4482216152605 / 16) * 0.3048", + "FromBaseToUnitFunc": "{x} / ((4.4482216152605 / 16) * 0.3048)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "ozf·ft", "oz·ft", "ozf-ft", "oz-ft" ] + } + ] + }, + { + "SingularName": "OunceForceInch", + "PluralName": "OunceForceInches", + "FromUnitToBaseFunc": "{x} * (4.4482216152605 / 16) * 2.54e-2", + "FromBaseToUnitFunc": "{x} / ((4.4482216152605 / 16) * 2.54e-2)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "ozf·in", "oz·in", "ozf-in", "oz-in", "in-oz" ] + } + ] + }, { "SingularName": "PoundalFoot", "PluralName": "PoundalFeet", @@ -77,7 +101,7 @@ "Localization": [ { "Culture": "en-US", - "Abbreviations": [ "lbf·in" ], + "Abbreviations": [ "lbf·in", "lb·in", "lbf-in", "lb-in", "in-lb" ], "AbbreviationsForPrefixes": { "Kilo": "kipf·in" } } ] @@ -91,7 +115,7 @@ "Localization": [ { "Culture": "en-US", - "Abbreviations": [ "lbf·ft" ], + "Abbreviations": [ "lbf·ft", "lb·ft", "lbf-ft", "lb-ft", "ft-lb" ], "AbbreviationsForPrefixes": { "Kilo": "kipf·ft" } } ] diff --git a/Common/UnitDefinitions/Volume.json b/Common/UnitDefinitions/Volume.json index b744737e5d..f9566e1f56 100644 --- a/Common/UnitDefinitions/Volume.json +++ b/Common/UnitDefinitions/Volume.json @@ -31,7 +31,6 @@ "PluralName": "CubicMeters", "FromUnitToBaseFunc": "{x}", "FromBaseToUnitFunc": "{x}", - "Prefixes": [ "Hecto", "Kilo" ], "BaseUnits": { "L": "Meter" }, @@ -46,6 +45,20 @@ } ] }, + { + "SingularName": "ThousandCubicMeter", + "PluralName": "ThousandCubicMeters", + "XmlDocSummary": "A count-style volume unit equal to 1000 cubic meters. This is distinct from CubicKilometer, which is (1000 m)³ or 1e9 cubic meters.", + "XmlDocRemarks": "Sources: https://energystar.my.site.com/PortfolioManager/s/article/Is-there-a-list-of-valid-property-level-water-meter-types-and-unit-of-measure-combinations-1748913622052, https://www.census.gov/foreign-trade/guide/sec4.html, https://www.bipm.org/en/measurement-units/si-prefixes", + "FromUnitToBaseFunc": "{x} * 1e3", + "FromBaseToUnitFunc": "{x} / 1e3", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "10³·m³", "kcm", "Kcm" ] + } + ] + }, { "SingularName": "CubicKilometer", "PluralName": "CubicKilometers", @@ -212,7 +225,6 @@ "XmlDocRemarks": "https://en.wikipedia.org/wiki/Cubic_foot", "FromUnitToBaseFunc": "{x} * 0.028316846592", "FromBaseToUnitFunc": "{x} / 0.028316846592", - "Prefixes": [ "Hecto", "Kilo", "Mega" ], "Localization": [ { "Culture": "en-US", @@ -224,6 +236,48 @@ } ] }, + { + "SingularName": "HundredCubicFoot", + "PluralName": "HundredCubicFeet", + "XmlDocSummary": "A count-style volume unit equal to 100 cubic feet, commonly abbreviated Ccf in natural gas and water utility billing.", + "XmlDocRemarks": "Sources: https://www.eia.gov/tools/faqs/faq.php?id=45&t=8, https://19january2017snapshot.epa.gov/www3/watersense/our_water/understanding_your_bill.html", + "FromUnitToBaseFunc": "{x} * 0.028316846592 * 1e2", + "FromBaseToUnitFunc": "{x} / (0.028316846592 * 1e2)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "Ccf", "hft³" ] + } + ] + }, + { + "SingularName": "ThousandCubicFoot", + "PluralName": "ThousandCubicFeet", + "XmlDocSummary": "A count-style volume unit equal to 1000 cubic feet, commonly abbreviated Mcf in natural gas reporting.", + "XmlDocRemarks": "Sources: https://www.eia.gov/tools/faqs/faq.php?id=45&t=8, https://natural-resources.canada.ca/climate-change/conversion-factors-common-units-used-north-american-cooperation-energy-information", + "FromUnitToBaseFunc": "{x} * 0.028316846592 * 1e3", + "FromBaseToUnitFunc": "{x} / (0.028316846592 * 1e3)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "Mcf", "kcf", "kft³" ] + } + ] + }, + { + "SingularName": "MillionCubicFoot", + "PluralName": "MillionCubicFeet", + "XmlDocSummary": "A count-style volume unit equal to 1,000,000 cubic feet, commonly abbreviated MMcf in natural gas reporting.", + "XmlDocRemarks": "Sources: https://natural-resources.canada.ca/climate-change/conversion-factors-common-units-used-north-american-cooperation-energy-information", + "FromUnitToBaseFunc": "{x} * 0.028316846592 * 1e6", + "FromBaseToUnitFunc": "{x} / (0.028316846592 * 1e6)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "MMcf", "Mft³" ] + } + ] + }, { "SingularName": "CubicInch", "PluralName": "CubicInches", diff --git a/Common/UnitDefinitions/VolumeFlow.json b/Common/UnitDefinitions/VolumeFlow.json index 0df447b2b5..3480bfc2e8 100644 --- a/Common/UnitDefinitions/VolumeFlow.json +++ b/Common/UnitDefinitions/VolumeFlow.json @@ -99,6 +99,22 @@ } ] }, + { + "SingularName": "CubicInchPerSecond", + "PluralName": "CubicInchesPerSecond", + "BaseUnits": { + "L": "Inch", + "T": "Second" + }, + "FromUnitToBaseFunc": "{x} * 1.6387064e-5", + "FromBaseToUnitFunc": "{x} / 1.6387064e-5", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "in³/s" ] + } + ] + }, { "SingularName": "CubicFootPerMinute", "PluralName": "CubicFeetPerMinute", @@ -115,6 +131,22 @@ } ] }, + { + "SingularName": "CubicInchPerMinute", + "PluralName": "CubicInchesPerMinute", + "BaseUnits": { + "L": "Inch", + "T": "Minute" + }, + "FromUnitToBaseFunc": "{x} * 1.6387064e-5 / 60", + "FromBaseToUnitFunc": "{x} / (1.6387064e-5 / 60)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "in³/min" ] + } + ] + }, { "SingularName": "CubicFootPerHour", "PluralName": "CubicFeetPerHour", @@ -466,6 +498,26 @@ } ] }, + { + "SingularName": "CubicMillimeterPerMinute", + "PluralName": "CubicMillimetersPerMinute", + "BaseUnits": { + "L": "Millimeter", + "T": "Minute" + }, + "FromUnitToBaseFunc": "{x} * 1e-9 / 60", + "FromBaseToUnitFunc": "{x} / (1e-9 / 60)", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "mm³/min" ] + }, + { + "Culture": "ru-RU", + "Abbreviations": [ "мм³/мин" ] + } + ] + }, { "SingularName": "AcreFootPerSecond", "PluralName": "AcreFeetPerSecond", diff --git a/Common/UnitEnumValues.g.json b/Common/UnitEnumValues.g.json index 3ebdc3d87d..a3ddd96516 100644 --- a/Common/UnitEnumValues.g.json +++ b/Common/UnitEnumValues.g.json @@ -86,7 +86,9 @@ "AreaDensity": { "KilogramPerSquareMeter": 1, "GramPerSquareMeter": 6, - "MilligramPerSquareMeter": 10 + "MilligramPerSquareMeter": 10, + "PoundPerThousandSquareFeet": 3, + "PoundPerSquareFoot": 8 }, "AreaMomentOfInertia": { "CentimeterToTheFourth": 1, @@ -469,7 +471,8 @@ "Poundal": 12, "PoundForce": 13, "ShortTonForce": 14, - "TonneForce": 15 + "TonneForce": 15, + "GramForce": 16 }, "ForceChangeRate": { "CentinewtonPerSecond": 1, @@ -567,7 +570,14 @@ "PoundPerSecondCubed": 15, "WattPerSquareFoot": 16, "WattPerSquareInch": 17, - "WattPerSquareMeter": 18 + "WattPerSquareMeter": 18, + "CentiwattPerSquareMillimeter": 22, + "DeciwattPerSquareMillimeter": 28, + "KilowattPerSquareMillimeter": 24, + "MicrowattPerSquareMillimeter": 20, + "MilliwattPerSquareMillimeter": 26, + "NanowattPerSquareMillimeter": 25, + "WattPerSquareMillimeter": 19 }, "HeatTransferCoefficient": { "BtuPerSquareFootDegreeFahrenheit": 1, @@ -575,7 +585,8 @@ "WattPerSquareMeterKelvin": 3, "BtuPerHourSquareFootDegreeFahrenheit": 11, "KilocaloriePerHourSquareMeterDegreeCelsius": 8, - "CaloriePerHourSquareMeterDegreeCelsius": 5 + "CaloriePerHourSquareMeterDegreeCelsius": 5, + "BtuPerSecondSquareInchDegreeFahrenheit": 4 }, "Illuminance": { "Kilolux": 1, @@ -1044,7 +1055,8 @@ "NanogramPerMole": 11, "PoundPerMole": 12, "KiloGramPerKiloMole": 20, - "KilogramPerKilomole": 15 + "KilogramPerKilomole": 15, + "PoundPerPoundMole": 24 }, "Permeability": { "HenryPerMeter": 1 @@ -1132,7 +1144,9 @@ "WattPerCubicFoot": 41, "WattPerCubicInch": 42, "WattPerCubicMeter": 43, - "WattPerLiter": 44 + "WattPerLiter": 44, + "BtuPerSecondCubicFoot": 47, + "BtuPerSecondCubicInch": 51 }, "PowerRatio": { "DecibelMilliwatt": 1, @@ -1188,7 +1202,9 @@ "Torr": 47, "MeterOfWaterColumn": 57, "CentimeterOfWaterColumn": 48, - "Millitorr": 52 + "Millitorr": 52, + "MilligramForcePerSquareFoot": 59, + "MilligramForcePerSquareMeter": 51 }, "PressureChangeRate": { "AtmospherePerSecond": 1, @@ -1370,9 +1386,9 @@ "PoundMassPerPoundForceHour": 4 }, "SpecificVolume": { + "CubicMillimeterPerKilogram": 3, "CubicFootPerPound": 1, - "CubicMeterPerKilogram": 2, - "MillicubicMeterPerKilogram": 3 + "CubicMeterPerKilogram": 2 }, "SpecificWeight": { "KilogramForcePerCubicCentimeter": 1, @@ -1489,7 +1505,8 @@ }, "ThermalConductivity": { "BtuPerHourFootFahrenheit": 1, - "WattPerMeterKelvin": 2 + "WattPerMeterKelvin": 2, + "BtuPerSecondInchFahrenheit": 6 }, "ThermalResistance": { "DegreeCelsiusPerWatt": 10, @@ -1520,7 +1537,9 @@ "PoundForceInch": 22, "TonneForceCentimeter": 23, "TonneForceMeter": 24, - "TonneForceMillimeter": 25 + "TonneForceMillimeter": 25, + "OunceForceFoot": 26, + "OunceForceInch": 27 }, "TorquePerLength": { "KilogramForceCentimeterPerMeter": 1, @@ -1571,21 +1590,20 @@ "DecausGallon": 17, "Deciliter": 18, "DeciusGallon": 19, - "HectocubicFoot": 20, - "HectocubicMeter": 21, + "HundredCubicFoot": 20, "Hectoliter": 22, "HectousGallon": 23, "ImperialBeerBarrel": 24, "ImperialGallon": 25, "ImperialOunce": 26, "ImperialPint": 27, - "KilocubicFoot": 28, - "KilocubicMeter": 29, + "ThousandCubicFoot": 28, + "ThousandCubicMeter": 29, "KiloimperialGallon": 30, "Kiloliter": 31, "KilousGallon": 32, "Liter": 33, - "MegacubicFoot": 34, + "MillionCubicFoot": 34, "MegaimperialGallon": 35, "Megaliter": 36, "MegausGallon": 37, @@ -1706,7 +1724,10 @@ "HectoliterPerDay": 74, "HectoliterPerHour": 72, "HectoliterPerMinute": 75, - "HectoliterPerSecond": 69 + "HectoliterPerSecond": 69, + "CubicInchPerMinute": 86, + "CubicInchPerSecond": 76, + "CubicMillimeterPerMinute": 80 }, "VolumeFlowPerArea": { "CubicFootPerMinutePerSquareFoot": 1, @@ -2018,5 +2039,13 @@ "SquareMeterKelvinPerKilowatt": 5, "SquareMeterKelvinPerWatt": 4, "SquareMillimeterKelvinPerWatt": 13 + }, + "AreaPerLength": { + "SquareCentimeterPerMeter": 10, + "SquareFootPerFoot": 4, + "SquareInchPerFoot": 3, + "SquareInchPerInch": 7, + "SquareMeterPerMeter": 1, + "SquareMillimeterPerMeter": 5 } } diff --git a/Common/UnitRelations.json b/Common/UnitRelations.json index bded89653c..9163188cdc 100644 --- a/Common/UnitRelations.json +++ b/Common/UnitRelations.json @@ -2,41 +2,66 @@ "1 = Area.SquareMeter * ReciprocalArea.InverseSquareMeter", "1 = Density.KilogramPerCubicMeter * SpecificVolume.CubicMeterPerKilogram", "1 = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter", + "1 = Frequency.PerSecond * Duration.Second", "1 = Length.Meter * ReciprocalLength.InverseMeter", "Acceleration.MeterPerSecondSquared = Jerk.MeterPerSecondCubed * Duration.Second", "AmountOfSubstance.Mole = MolarFlow.MolePerSecond * Duration.Second", "AmountOfSubstance.Mole = Molarity.MolePerCubicMeter * Volume.CubicMeter", "Angle.Radian = RotationalSpeed.RadianPerSecond * Duration.Second", + "Area.SquareMeter = AreaPerLength.SquareMeterPerMeter * Length.Meter -- NoInferredDivision", "Area.SquareMeter = KinematicViscosity.SquareMeterPerSecond * Duration.Second -- NoInferredDivision", "Area.SquareMeter = Length.Meter * Length.Meter", "Area.SquareMeter = Volume.CubicMeter * ReciprocalLength.InverseMeter", + "AreaDensity.KilogramPerSquareMeter = Density.KilogramPerCubicMeter * Length.Meter", + "AreaMomentOfInertia.MeterToTheFourth = Area.SquareMeter * Area.SquareMeter", "AreaMomentOfInertia.MeterToTheFourth = Volume.CubicMeter * Length.Meter", - "double = SpecificEnergy.JoulePerKilogram * BrakeSpecificFuelConsumption.KilogramPerJoule", + "BitRate.BitPerSecond = Information.Bit * Frequency.PerSecond", + "DoseAreaProduct.GraySquareMeter = AbsorbedDoseOfIonizingRadiation.Gray * Area.SquareMeter", "DynamicViscosity.NewtonSecondPerMeterSquared = Density.KilogramPerCubicMeter * KinematicViscosity.SquareMeterPerSecond", + "ElectricApparentEnergy.VoltampereHour = ElectricApparentPower.Voltampere * Duration.Hour", "ElectricCharge.AmpereHour = ElectricCurrent.Ampere * Duration.Hour", + "ElectricCharge.Coulomb = ElectricCapacitance.Farad * ElectricPotential.Volt", + "ElectricCharge.Coulomb = ElectricChargeDensity.CoulombPerCubicMeter * Volume.CubicMeter", + "ElectricCharge.Coulomb = ElectricSurfaceChargeDensity.CoulombPerSquareMeter * Area.SquareMeter", + "ElectricCurrent.Ampere = ElectricCurrentDensity.AmperePerSquareMeter * Area.SquareMeter", "ElectricCurrent.Ampere = ElectricCurrentGradient.AmperePerSecond * Duration.Second", + "ElectricCurrent.Ampere = ElectricPotential.Volt * ElectricConductance.Siemens", "ElectricPotential.Volt = ElectricCurrent.Ampere * ElectricResistance.Ohm", + "ElectricPotential.Volt = ElectricField.VoltPerMeter * Length.Meter", + "ElectricPotential.Volt = ElectricPotentialChangeRate.VoltPerSecond * Duration.Second", + "ElectricReactiveEnergy.VoltampereReactiveHour = ElectricReactivePower.VoltampereReactive * Duration.Hour", "Energy.Joule = ElectricPotential.Volt * ElectricCharge.Coulomb", "Energy.Joule = EnergyDensity.JoulePerCubicMeter * Volume.CubicMeter", "Energy.Joule = MolarEnergy.JoulePerMole * AmountOfSubstance.Mole", "Energy.Joule = Power.Watt * Duration.Second", "Energy.Joule = SpecificEnergy.JoulePerKilogram * Mass.Kilogram", "Energy.Joule = TemperatureDelta.Kelvin * Entropy.JoulePerKelvin", + "EnergyDensity.JoulePerCubicMeter = VolumetricHeatCapacity.JoulePerCubicMeterKelvin * TemperatureDelta.Kelvin", "Entropy.JoulePerKelvin = SpecificEntropy.JoulePerKilogramKelvin * Mass.Kilogram", "Force.Newton = ForceChangeRate.NewtonPerSecond * Duration.Second", "Force.Newton = ForcePerLength.NewtonPerMeter * Length.Meter", "Force.Newton = Mass.Kilogram * Acceleration.MeterPerSecondSquared", "Force.Newton = Pressure.Pascal * Area.SquareMeter", + "Force.Newton = SpecificWeight.NewtonPerCubicMeter * Volume.CubicMeter", + "ForcePerLength.NewtonPerMeter = Acceleration.MeterPerSecondSquared * LinearDensity.KilogramPerMeter", "ForcePerLength.NewtonPerMeter = Force.Newton * ReciprocalLength.InverseMeter", "ForcePerLength.NewtonPerMeter = Pressure.NewtonPerSquareMeter * Length.Meter", "ForcePerLength.NewtonPerMeter = SpecificWeight.NewtonPerCubicMeter * Area.SquareMeter", + "HeatFlux.WattPerSquareMeter = HeatTransferCoefficient.WattPerSquareMeterKelvin * TemperatureDelta.Kelvin", + "Impulse.NewtonSecond = Force.Newton * Duration.Second", + "Information.Bit = BitRate.BitPerSecond * Duration.Second", + "Irradiation.JoulePerSquareMeter = Irradiance.WattPerSquareMeter * Duration.Second", "KinematicViscosity.SquareMeterPerSecond = Length.Meter * Speed.MeterPerSecond", + "LeakRate.PascalCubicMeterPerSecond = Pressure.Pascal * VolumeFlow.CubicMeterPerSecond", "Length.Meter = Area.SquareMeter * ReciprocalLength.InverseMeter", "Length.Meter = Speed.MeterPerSecond * Duration.Second", "Length.Meter = Volume.CubicMeter * ReciprocalArea.InverseSquareMeter", "LinearDensity.KilogramPerMeter = Area.SquareMeter * Density.KilogramPerCubicMeter", + "LinearDensity.KilogramPerMeter = AreaDensity.KilogramPerSquareMeter * Length.Meter", "LuminousFlux.Lumen = Illuminance.Lux * Area.SquareMeter", + "LuminousFlux.Lumen = LuminousIntensity.Candela * SolidAngle.Steradian", "LuminousIntensity.Candela = Luminance.CandelaPerSquareMeter * Area.SquareMeter", + "MagneticFlux.Weber = MagneticField.Tesla * Area.SquareMeter", "Mass.Kilogram = AmountOfSubstance.Mole * MolarMass.KilogramPerMole", "Mass.Kilogram = AreaDensity.KilogramPerSquareMeter * Area.SquareMeter", "Mass.Kilogram = Density.KilogramPerCubicMeter * Volume.CubicMeter", @@ -57,17 +82,25 @@ "Power.Watt = Energy.Joule * Frequency.PerSecond", "Power.Watt = Force.Newton * Speed.MeterPerSecond", "Power.Watt = HeatFlux.WattPerSquareMeter * Area.SquareMeter", + "Power.Watt = LinearPowerDensity.WattPerMeter * Length.Meter", + "Power.Watt = PowerDensity.WattPerCubicMeter * Volume.CubicMeter", "Power.Watt = SpecificEnergy.JoulePerKilogram * MassFlow.KilogramPerSecond", "Power.Watt = Torque.NewtonMeter * RotationalSpeed.RadianPerSecond", "Pressure.NewtonPerSquareMeter = Force.Newton * ReciprocalArea.InverseSquareMeter", "Pressure.NewtonPerSquareMeter = ForcePerLength.NewtonPerMeter * ReciprocalLength.InverseMeter", + "Pressure.Pascal = Acceleration.MeterPerSecondSquared * AreaDensity.KilogramPerSquareMeter", + "Pressure.Pascal = FluidResistance.PascalSecondPerCubicMeter * VolumeFlow.CubicMeterPerSecond", "Pressure.Pascal = PressureChangeRate.PascalPerSecond * Duration.Second", "Pressure.Pascal = SpecificWeight.NewtonPerCubicMeter * Length.Meter", + "QuantityValue = SpecificEnergy.JoulePerKilogram * BrakeSpecificFuelConsumption.KilogramPerJoule", "RadiationEquivalentDose.Sievert = RadiationEquivalentDoseRate.SievertPerHour * Duration.Hour", "Ratio.DecimalFraction = Area.SquareMeter * ReciprocalArea.InverseSquareMeter -- NoInferredDivision", + "Ratio.DecimalFraction = Compressibility.InversePascal * Pressure.Pascal", + "Ratio.DecimalFraction = RatioChangeRate.DecimalFractionPerSecond * Duration.Second", "Ratio.DecimalFraction = TemperatureDelta.Kelvin * CoefficientOfThermalExpansion.PerKelvin -- NoInferredDivision", "ReciprocalArea.InverseSquareMeter = ReciprocalLength.InverseMeter * ReciprocalLength.InverseMeter", "ReciprocalLength.InverseMeter = Length.Meter * ReciprocalArea.InverseSquareMeter", + "RotationalSpeed.RadianPerSecond = RotationalAcceleration.RadianPerSecondSquared * Duration.Second", "RotationalStiffness.NewtonMeterPerRadian = RotationalStiffnessPerLength.NewtonMeterPerRadianPerMeter * Length.Meter", "SpecificEnergy.JoulePerKilogram = SpecificEntropy.JoulePerKilogramKelvin * TemperatureDelta.Kelvin", "SpecificEnergy.JoulePerKilogram = Speed.MeterPerSecond * Speed.MeterPerSecond", @@ -75,11 +108,15 @@ "Speed.MeterPerSecond = Acceleration.MeterPerSecondSquared * Duration.Second", "TemperatureDelta.DegreeCelsius = TemperatureChangeRate.DegreeCelsiusPerSecond * Duration.Second", "TemperatureDelta.DegreeCelsius = TemperatureGradient.DegreeCelsiusPerKilometer * Length.Kilometer", + "TemperatureDelta.Kelvin = ThermalInsulance.SquareMeterKelvinPerWatt * HeatFlux.WattPerSquareMeter", + "TemperatureDelta.Kelvin = ThermalResistance.KelvinPerWatt * Power.Watt", "Torque.NewtonMeter = ForcePerLength.NewtonPerMeter * Area.SquareMeter", "Torque.NewtonMeter = Length.Meter * Force.Newton", "Torque.NewtonMeter = RotationalStiffness.NewtonMeterPerRadian * Angle.Radian", "Volume.CubicMeter = Length.Meter * Area.SquareMeter", "Volume.CubicMeter = SpecificVolume.CubicMeterPerKilogram * Mass.Kilogram", "Volume.CubicMeter = VolumeFlow.CubicMeterPerSecond * Duration.Second", - "VolumeFlow.CubicMeterPerSecond = Area.SquareMeter * Speed.MeterPerSecond" + "Volume.CubicMeter = VolumePerLength.CubicMeterPerMeter * Length.Meter -- NoInferredDivision", + "VolumeFlow.CubicMeterPerSecond = Area.SquareMeter * Speed.MeterPerSecond", + "VolumeFlow.CubicMeterPerSecond = VolumeFlowPerArea.CubicMeterPerSecondPerSquareMeter * Area.SquareMeter -- NoInferredDivision" ] \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index d8f549a773..4ead857691 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,9 +3,8 @@ $(MSBuildThisFileDirectory)Artifacts/$(MSBuildProjectName) - - $(MSBuildThisFileDirectory)Artifacts/UnitsNet.NanoFramework/$(MSBuildProjectName) - + + $(MSBuildThisFileDirectory)Artifacts/Nugets @@ -32,9 +31,4 @@ true snupkg - - - - - diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000000..9ded9b351a --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 4a94c2ffd0..34d9dc7b16 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,10 +4,14 @@ + + - + + - + + @@ -16,5 +20,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + - \ No newline at end of file + diff --git a/Directory.Solution.targets b/Directory.Solution.targets new file mode 100644 index 0000000000..966654a49f --- /dev/null +++ b/Directory.Solution.targets @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/Docs/README.md b/Docs/README.md index 06b0314dde..520c47e867 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -3,6 +3,7 @@ ## Contributing - [Adding a New Quantity or Unit](adding-a-new-unit.md) — step-by-step guide for adding quantities and units +- [Quantity and Unit Definition Schema](quantity-and-unit-definition-schema.md) — field reference for quantity and unit JSON definitions - [Adding Operator Overloads](adding-operator-overloads.md) — how to add strongly-typed arithmetic operators - [Precision](precision.md) — how conversions work and their precision limits @@ -15,7 +16,6 @@ ## Platform Support -- [.NET nanoFramework](nanoframework.md) — embedded device support - [Experimental: Generic Math](experimental-generic-math.md) — .NET 7+ generic math interfaces ## Upgrade Guides diff --git a/Docs/adding-a-new-unit.md b/Docs/adding-a-new-unit.md index 93534442a2..92decc0005 100644 --- a/Docs/adding-a-new-unit.md +++ b/Docs/adding-a-new-unit.md @@ -28,6 +28,8 @@ Sometimes we just have to say no, sorry! We simply want to avoid bloating the li - [x] Can be represented by a `double` numeric value, integer values are not well supported and may suffer from precision errors - [x] Is not [dimensionless/unitless](https://en.wikipedia.org/wiki/Dimensionless_quantity) (consider using `Ratio`) +Single-unit quantities are the exception. If a proposed quantity has only one unit, it needs stronger justification: the quantity must be widely used as a typed quantity representation on its own, not just as a unit abbreviation with no conversions. + ### A unit is a good fit to add to a quantity, if it - [x] Is well documented and unambiguous, e.g. has a wiki page or found in online unit converters @@ -51,6 +53,7 @@ Ok, enough of that. Let's move on! ## Quick Summary of Steps Units.NET uses [CodeGen](https://github.com/angularsen/UnitsNet/tree/master/CodeGen), a C# command line app that reads [JSON files with quantity and unit definitions](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions) and generates C# code. +See the [Quantity and Unit Definition Schema](quantity-and-unit-definition-schema.md) for a field-by-field reference. To add a quantity or a unit: @@ -66,6 +69,7 @@ Not too difficult. Below are the detailed steps. * Place in [Common/UnitDefinitions](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions) * See [Length.json](https://github.com/angularsen/UnitsNet/tree/master/Common/UnitDefinitions/Length.json) as an example. +* Use the [Quantity and Unit Definition Schema](quantity-and-unit-definition-schema.md) as the reference for supported properties and values. * Use reliable references, such as [UN/ECE Recommendation No. 21](https://unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf), Google, Wolfram Alpha or online converters. #### Conversion function conventions @@ -86,6 +90,9 @@ Prefer the most widely used abbreviation in the domain, but try to adapt to our * Use `/` over `⁻¹`, such as `km/h` and `J/(mol·K)` * Use `h` for hours, `min` for minutes and `s` for seconds (`m` is ambiguous with meters) * Use abbreviations defined by [SI Unit System](https://en.wikipedia.org/wiki/International_System_of_Units), such as `l` instead of `L` for liters +* For force-derived compound units, prefer the technically precise force abbreviation as the primary abbreviation: `lbf`, `ozf`, `gf`, etc. Common shorthand variants such as `lb`, `oz`, or `g` may be added as secondary abbreviations when they are widely used and unambiguous for that quantity. For example, `lbf·ft` should be the primary abbreviation for pound-force foot torque, but `lb·ft` can be accepted as a parsing alias. Do not add shorthand aliases if they would be ambiguous within the same quantity. +* If a common shorthand abbreviation fits multiple quantities, add it to each relevant quantity, e.g. `ft-lb` for torque and energy, or `oz·in` for torque and static unbalance if both are supported. Global parsing should be ambiguous, while quantity-specific parsing still works. +* Prefer singular unit symbols such as `lb` and `oz`, not pluralized forms such as `lbs` and `ozs`. A pluralized form can be accepted as a secondary abbreviation if it is a strong domain convention, but it should not normally be the primary abbreviation. * Use suffixes to distinguish variants of similar units, such as `gal (U.S.)` vs `gal (imp.)` for gallons * `(U.S.)` for United States * `(imp.)` for imperial / British units @@ -100,7 +107,7 @@ The [7 SI base units](https://en.wikipedia.org/wiki/SI_base_unit#Seven_SI_base_u - `M` - Mass - `T` - Time - `I` - ElectricCurrent -- `Theta` - Temperature +- `Θ` - Temperature - `N` - AmountOfSubstance - `J` - LuminousIntensity @@ -206,6 +213,7 @@ git push ## Logarithmic Units Units.NET supports logarithmic units by adding `Logarithmic` and `LogarithmicScalingFactor` (optional) properties. +See [Logarithmic quantities](quantity-and-unit-definition-schema.md#logarithmic-quantities) for the exact conversion model and examples. * `LogarithmicScalingFactor` is used to provide a scaling factor in the logarithmic conversion. For example, a scaling factor of `2` is required when implementing the ratio of the squares of two field amplitude quantities such as voltage. In most cases `LogarithmicScalingFactor` will be `1`. diff --git a/Docs/adding-operator-overloads.md b/Docs/adding-operator-overloads.md index 46f453a0a7..3ea8eb80d7 100644 --- a/Docs/adding-operator-overloads.md +++ b/Docs/adding-operator-overloads.md @@ -2,6 +2,15 @@ There is a large number of operator overloads, to facilitate strongly typed computations such as `Speed speed = Length.FromMeters(100) / TimeSpan.FromSeconds(9)`. +Operators that can be expressed as a multiplication, division, or inverse relation may be generated from +`Common/UnitRelations.json`; see [Quantity relations](quantity-and-unit-definition-schema.md#quantity-relations). + +For generated relation tests, use the existing relation-test ownership pattern: + +- Multiplication tests go in `Tests.cs`, where `LeftQuantity` is the first operand in the explicit `UnitRelations.json` entry. Put the inferred reversed multiplication test there too. +- Inferred division tests go in `Tests.cs`, because the generated division operators are exposed by the result quantity. +- Inverse relation tests go in each quantity's own test file, because each side gets its own `Inverse()` method. + 1. Put operator overload in `Length.extra.cs` if the **first parameter** is `Length` 2. Add a short xmldoc summary as per the example below. You can add more descriptions if it is useful. 3. Add a unit test case and place it in the equivalent file `LengthTests.cs`. diff --git a/Docs/extending-with-custom-units.md b/Docs/extending-with-custom-units.md index 0ff86eb36e..cfbba73989 100644 --- a/Docs/extending-with-custom-units.md +++ b/Docs/extending-with-custom-units.md @@ -1,15 +1,31 @@ # Extending with Custom Units +## Recommended prototype: UnitsNet.Modular + +[UnitsNet.Modular](../UnitsNet.Modular/README.md) is our prototype for a better way to add +application-specific quantities and units. It is a proof of concept and currently in pre-release, +but it generates strongly typed quantity structs, unit enums, conversions, parsing, formatting, and +metadata from your JSON definitions at compile time. + +Start with [Add custom quantities](../UnitsNet.Modular/README.md#add-custom-quantities), or explore the +[UnitsNet.Modular samples in GitHub Codespaces](https://codespaces.new/angularsen/UnitsNet?devcontainer_path=.devcontainer%2Funitsnet-modular%2Fdevcontainer.json&quickstart=1). + +`UnitsNet` and `UnitsNet.Modular` are alternative implementations and cannot be referenced together +in the same consumer project. If you need to keep using the established `UnitsNet` package, the +runtime approach below remains available. + +## Secondary approach: runtime custom quantities in UnitsNet + This article is for when you want to add your own custom quantities and units at runtime, not included in the UnitsNet nuget. To add new quantities or units to the `UnitsNet` nuget, please see [Adding a New Unit](adding-a-new-unit.md). -## Disclaimer: This is highly experimental and incomplete +### Disclaimer: This is highly experimental and incomplete You miss out on the statically generated code for members like `Length.FromMeters(1)` and `myLength.Meters`. Conversion methods like `myLength.As()` and `myLength.ToUnit()` currently only support their respective unit enums, in this case `LengthUnit`. -## Can I add a custom unit to an existing quantity in UnitsNet? +### Can I add a custom unit to an existing quantity in UnitsNet? Currently, no. @@ -21,14 +37,14 @@ Since UnitsNet is so statically typed, your options are limited to: 1. Submit a pull request to [add a new unit](adding-a-new-unit.md) to the UnitsNet nuget 2. Build your own custom version of UnitsNet -## Why add a custom quantity? +### Why add a custom quantity? Good question. In its current state, the support for custom quantities and units is limited and provides limited integration with the existing units and code. We consider it exploratory, to see what is possible, and we welcome ideas on how it can be improved. -### Key benefits +#### Key benefits - Reuse functionality that operates on `IQuantity` - Dynamically convert to unit with `.As(Enum)` @@ -38,25 +54,25 @@ We consider it exploratory, to see what is possible, and we welcome ideas on how - Also allows you to dynamically convert between your custom units and the built-in units, such as `CustomLengthUnit.ElbowToThumb` to `LengthUnit.Meter`. - Reuse `QuantityParser` and `UnitParser` to parse quantity strings like "5 cm" and "cm" for your own quantities and units -### What could be better +#### What could be better - Source generators via nuget, if possible [Using source generators #902](https://github.com/angularsen/UnitsNet/issues/902) - String-based lookup instead of enum-based for quantity methods like `As()` and `ToUnit()`, required for [XP One nuget per quantity #1181](https://github.com/angularsen/UnitsNet/pull/1181) Got more ideas? Create a discussion or issue. -## Units.NET structure +### Units.NET structure Units.NET roughly consists of these parts: * Quantities like `Length` and `Force` * Unit enum values like `LengthUnit.Meter` and `ForceUnit.Newton` * `UnitAbbreviationsCache`, `UnitParser`, `QuantityParser` and `UnitConverter` for parsing and converting quantities and units -* JSON files for defining units, conversion functions and abbreviations +* [JSON files for defining units, conversion functions and abbreviations](quantity-and-unit-definition-schema.md) * `CodeGen` console app to generate C# code based on JSON files -## Example: Custom quantity `HowMuch` with units `HowMuchUnit` +### Example: Custom quantity `HowMuch` with units `HowMuchUnit` -### Sample output +#### Sample output ``` GetDefaultAbbreviation(): sm, lts, tns Parse(): Some, Lots, Tons @@ -67,7 +83,7 @@ Convert 10 tons to: 10 tns ``` -### Map unit enum values to unit abbreviations +#### Map unit enum values to unit abbreviations ```c# UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Some, "sm"); @@ -75,7 +91,7 @@ UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Lots, "l UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(HowMuchUnit.Tons, "tns"); ``` -### Lookup unit abbreviations from enum values +#### Lookup unit abbreviations from enum values ```c# Console.WriteLine("GetDefaultAbbreviation(): " + string.Join(", ", @@ -85,7 +101,7 @@ Console.WriteLine("GetDefaultAbbreviation(): " + string.Join(", ", )); ``` -### Parse unit abbreviations back to enum values +#### Parse unit abbreviations back to enum values ```c# Console.WriteLine("Parse(): " + string.Join(", ", @@ -95,7 +111,7 @@ Console.WriteLine("Parse(): " + string.Join(", ", )); ``` -### Convert between units of custom quantity +#### Convert between units of custom quantity ```c# var unitConverter = UnitConverter.Default; @@ -112,7 +128,7 @@ Console.WriteLine(Convert(HowMuchUnit.Lots)); // 100 lts Console.WriteLine(Convert(HowMuchUnit.Tons)); // 10 tns ``` -### Sample quantity +#### Sample quantity See the sample implementation in the test suite: - [HowMuchUnit.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet.Tests/CustomQuantities/HowMuchUnit.cs) diff --git a/Docs/nanoframework.md b/Docs/nanoframework.md deleted file mode 100644 index d1aca78d8b..0000000000 --- a/Docs/nanoframework.md +++ /dev/null @@ -1,19 +0,0 @@ -# .NET nanoFramework - -.NET nanoFramework is a free and open-source platform that enables the writing of managed code applications for constrained embedded devices. It is suitable for many types of projects including IoT sensors, wearables, academic proof of concept, robotics, hobbyist/makers creations or even complex industrial equipment. - -https://www.nanoframework.net/ - -## Update nanoFramework dependencies - -Units.NET publishes quantities as individual nuget packages, which means there is a large number of projects to maintain. -To overcome this, the CodeGen project generates the solution file, project files and .nuspec files - in addition to source code. - -The dependencies are hard coded in the code generator, so in order to update the dependencies you must specify an extra flag. - -```sh -cd CodeGen -dotnet run --update-nano-framework-dependencies -``` - -As of this writing, `mscorlib` and `System.Math` are the two nuget dependencies to update. diff --git a/Docs/precision.md b/Docs/precision.md index 6f929d4798..668b19cccf 100644 --- a/Docs/precision.md +++ b/Docs/precision.md @@ -4,13 +4,30 @@ Units.NET was not designed for high-precision, but rather a tool of convenience - A base unit is chosen for all quantities - SI base unit is preferred where available, such as `LengthUnit.Meter` and `VolumeUnit.CubicMeter`. + - See the [`BaseUnit` schema reference](quantity-and-unit-definition-schema.md#quantity-object) for how it is declared. - `MassUnit.Gram` was chosen to better support SI prefixes like `kilo`, `mega` etc. - The value is typically represented by a `double` value (64-bit) - Conversions go via the base unit. - Centimeter => Meter => Kilometer - As a result, most conversions have a rounding error. The error is larger for units that are way larger or way smaller than the base unit. - A rounding error of `1e-5` is accepted for round-trip conversion of most units in the library. In many use cases this is sufficient, but for others this may not be acceptable. - - There is support for [custom conversion functions](https://github.com/angularsen/UnitsNet#convert-between-units-of-custom-quantity) between unit A to unit B, typically to add 3rd party units. This can also be used to improve the precision for specific conversions since it no longer converts via the base unit. + - In v6, unit conversion definitions can be customized before the converter is built. This can be used to override a built-in conversion factor or add conversion functions for custom quantities. + +## Overriding built-in unit conversions + +Built-in unit definitions can be customized through `UnitsNetSetup.ConfigureDefaults()` before the default setup is used: + +```csharp +UnitsNetSetup.ConfigureDefaults(builder => builder.ConfigureQuantity(() => + Pressure.PressureInfo.CreateDefault(units => + units.Configure(PressureUnit.InchOfWaterColumn, unit => + unit.WithConversionFactorFromBase(999))))); + +var pressure = Pressure.FromPascals(1); +double value = pressure.As(PressureUnit.InchOfWaterColumn); // 999 +``` + +For isolated conversions, create a custom `QuantityInfo` and pass it to a custom `UnitConverter` instead of changing the global defaults. See `Samples/UnitsNetSetup.Configuration/ConfigureWithCustomConversions.cs` for a complete example. ## Test precision diff --git a/Docs/quantity-and-unit-definition-schema.md b/Docs/quantity-and-unit-definition-schema.md new file mode 100644 index 0000000000..9b3752583f --- /dev/null +++ b/Docs/quantity-and-unit-definition-schema.md @@ -0,0 +1,365 @@ +# Quantity and Unit Definition Schema + +Units.NET quantity and unit definitions are JSON files in +[`Common/UnitDefinitions`](../Common/UnitDefinitions). Each file describes one quantity, the units that belong to it, +their conversions, abbreviations, and optional code-generation behavior. + +This is the contributor-facing schema reference for those files. The deserialization types in +[`CodeGen/JsonTypes`](../CodeGen/JsonTypes) and the code generator are the implementation source of truth. + +> [!IMPORTANT] +> The current JSON deserializer is permissive: unknown properties are ignored and some missing properties are not +> rejected until generated code is compiled or tested. Treat properties marked as required here as required, and do +> not rely on unknown properties being accepted. + +For the contribution workflow and style conventions, see +[Adding a New Quantity or Unit](adding-a-new-unit.md). + +## Minimal definition + +The following is a minimal definition of a linear quantity with one unit: + +```json +{ + "Name": "Length", + "BaseUnit": "Meter", + "XmlDocSummary": "Length is a measure of distance.", + "BaseDimensions": { + "L": 1 + }, + "Units": [ + { + "SingularName": "Meter", + "PluralName": "Meters", + "BaseUnits": { + "L": "Meter" + }, + "FromUnitToBaseFunc": "{x}", + "FromBaseToUnitFunc": "{x}", + "Localization": [ + { + "Culture": "en-US", + "Abbreviations": [ "m" ] + } + ] + } + ] +} +``` + +By convention, the filename is `.json`, such as `Length.json`. Quantity and unit names use PascalCase and must +be valid C# identifiers because they become generated type and member names. + +## UnitsNet.Modular compatibility + +UnitsNet.Modular adds the optional `Namespace` field. It defaults to `UnitsNet` and becomes part of the stable +semantic ID `Namespace.Name`; use a namespace you own for custom and third-party definitions. Repository CodeGen +ignores this field. + +Add a custom definition to a Modular project as a Roslyn `AdditionalFiles` item. See +[Add custom quantities](../UnitsNet.Modular/README.md#add-custom-quantities) for registration and selection. + +## Quantity object + +The root JSON object represents a quantity. + +| Property | Type | Required/default | Description | +|---|---|---|---| +| `Name` | string | Required | PascalCase quantity name used for the generated quantity type and unit enum. Conventionally matches the filename. | +| `Namespace` | string | `UnitsNet` | UnitsNet.Modular only. Namespace for generated types and part of the stable semantic ID `Namespace.Name`. Ignored by repository CodeGen. | +| `BaseUnit` | string | Required | `SingularName` of the unit through which conversions are performed. It must identify exactly one entry in `Units`. | +| `XmlDocSummary` | string | Required | XML documentation summary for the generated quantity type. XML documentation elements such as `` may be used. | +| `XmlDocRemarks` | string | Optional | Additional XML documentation remarks for the generated quantity type. Often contains a reference URL. | +| `BaseDimensions` | object | All exponents default to `0` | Exponents of the seven SI base dimensions. See [Base dimensions](#base-dimensions). | +| `AffineOffsetType` | string | Optional | Marks an affine quantity and names the quantity used to represent differences, such as `TemperatureDelta` for `Temperature`. | +| `Logarithmic` | boolean-like | `false` | Generates logarithmic arithmetic and implements `ILogarithmicQuantity`. Existing definitions use the legacy string `"True"`; a JSON boolean is also accepted by the current deserializer. | +| `LogarithmicScalingFactor` | integer-like | `1` | Multiplier applied to the conventional factor of 10 for logarithmic arithmetic. Existing definitions use strings such as `"1"` and `"2"`; JSON integers are also accepted. Only meaningful when `Logarithmic` is true. See [Logarithmic quantities](#logarithmic-quantities). | +| `ObsoleteText` | string | Optional | Generates an `Obsolete` attribute with this message for the quantity and its generated numeric extension methods. | +| `Units` | array of unit objects | Required | Units belonging to the quantity. At least one unit is required, and one must match `BaseUnit`. | + +`AffineOffsetType` and `Logarithmic` describe different arithmetic models and must not be combined. + +### Base dimensions + +`BaseDimensions` maps SI dimension symbols to integer exponents. Missing dimensions have exponent zero. + +| Key | Dimension | Example | +|---|---|---| +| `L` | Length | `Length`: `{ "L": 1 }` | +| `M` | Mass | `Density`: `{ "M": 1, "L": -3 }` | +| `T` | Time | `Frequency`: `{ "T": -1 }` | +| `I` | Electric current | `ElectricCurrent`: `{ "I": 1 }` | +| `Θ` | Thermodynamic temperature | `Temperature`: `{ "Θ": 1 }` | +| `N` | Amount of substance | `AmountOfSubstance`: `{ "N": 1 }` | +| `J` | Luminous intensity | `LuminousIntensity`: `{ "J": 1 }` | + +The temperature key is the Greek capital theta `Θ`, not the word `Theta`. Dimensionless quantities may omit +`BaseDimensions` or use an empty object. + +`BaseDimensions` describes the quantity's dimensional exponents. It is distinct from: + +- `BaseUnit`, the intermediate unit used for conversions. +- A unit's `BaseUnits`, the concrete SI base-unit choices used for unit-system selection. + +## Unit object + +Each entry in `Units` represents one unit. + +| Property | Type | Required/default | Description | +|---|---|---|---| +| `SingularName` | string | Required | PascalCase singular unit name. Becomes an enum member and part of generated factory and conversion member names. Must be unique within the quantity. | +| `PluralName` | string | Required | PascalCase plural unit name. Used in generated member names such as `Length.Meters`. | +| `FromUnitToBaseFunc` | string | Required | C# expression that converts `{x}` from this unit to the quantity's `BaseUnit`. | +| `FromBaseToUnitFunc` | string | Required | Inverse C# expression that converts `{x}` from the quantity's `BaseUnit` to this unit. | +| `BaseUnits` | object | Optional | Concrete SI base-unit names used to match this unit to a `UnitSystem`. See [Base units](#base-units). | +| `Prefixes` | array of strings | Empty | Prefix names for which CodeGen generates additional units. See [Prefixes](#prefixes). | +| `Localization` | array of localization objects | Required | Culture-specific abbreviations. Every unit should define `en-US`. | +| `XmlDocSummary` | string | Optional | XML documentation summary for the generated unit enum member. | +| `XmlDocRemarks` | string | Optional | Additional XML documentation remarks for the generated unit enum member. Often contains a reference URL. | +| `ObsoleteText` | string | Optional | Generates an `Obsolete` attribute with this message for the unit and its generated numeric extension methods. | + +The base-unit entry should use identity conversion expressions: + +```json +"FromUnitToBaseFunc": "{x}", +"FromBaseToUnitFunc": "{x}" +``` + +### Conversion expressions + +Conversions always go through the quantity's `BaseUnit`: + +```text +source unit --FromUnitToBaseFunc--> BaseUnit --FromBaseToUnitFunc--> target unit +``` + +The conversion properties contain C# numeric expressions. CodeGen replaces every `{x}` placeholder with the input +value and emits the expression into generated C# code. Expressions may use numeric literals, arithmetic operators, +parentheses, and available APIs such as `Math.PI`. + +The two expressions must be inverses. For example, with meters as the base unit: + +```json +{ + "SingularName": "Centimeter", + "PluralName": "Centimeters", + "FromUnitToBaseFunc": "{x} / 100", + "FromBaseToUnitFunc": "{x} * 100" +} +``` + +Affine units include an offset: + +```json +{ + "SingularName": "DegreeCelsius", + "PluralName": "DegreesCelsius", + "FromUnitToBaseFunc": "{x} + 273.15", + "FromBaseToUnitFunc": "{x} - 273.15" +} +``` + +Use `{x}` exactly, preserve exact constituent constants where possible, and follow the +[conversion function conventions](adding-a-new-unit.md#conversion-function-conventions). Since expressions are C# +source rather than a language-neutral expression format, other platform generators must translate the supported +expression syntax. + +### Base units + +`BaseUnits` maps the same seven dimension keys to singular unit names from the corresponding SI base quantities. +It allows APIs such as `new Length(1, UnitSystem.SI)` to select a suitable unit. + +For example, the newton has: + +```json +"BaseUnits": { + "L": "Meter", + "M": "Kilogram", + "T": "Second" +} +``` + +Only the concrete unit choice is stored; dimensional exponents come from the quantity's `BaseDimensions`. For example, +an area unit can use `{ "L": "Centimeter" }` even though the length dimension has exponent 2. + +`BaseUnits` may be omitted when no meaningful mapping exists, such as for a gallon or a dimensionless ratio. + +### Prefixes + +`Prefixes` tells CodeGen to derive additional units from the current unit. For example: + +```json +"Prefixes": [ "Milli", "Kilo", "Mega" ] +``` + +For each prefix, CodeGen: + +- Creates singular and plural names by prepending the prefix. +- Adjusts both conversion expressions by the prefix factor. +- Prefixes each localized abbreviation, unless an explicit override is configured. +- Attempts to infer prefixed `BaseUnits`. + +Accepted metric prefixes are: + +`Yocto`, `Zepto`, `Atto`, `Femto`, `Pico`, `Nano`, `Micro`, `Milli`, `Centi`, `Deci`, `Deca`, `Hecto`, `Kilo`, +`Mega`, `Giga`, `Tera`, `Peta`, `Exa`, `Zetta`, and `Yotta`. + +Accepted binary prefixes are: + +`Kibi`, `Mebi`, `Gibi`, `Tebi`, `Pebi`, and `Exbi`. + +Do not also define a generated prefixed unit explicitly. + +Do not use `Prefixes` when the prefix would apply to only part of a powered or compound unit and change the meaning, +or when it would create an abbreviation already used by another unit of the same quantity. Define explicit units +instead. For example, define `CubicMillimeterPerKilogram` for `mm³/kg` instead of generating +`MillicubicMeterPerKilogram`, and define `ThousandCubicMeter` with an abbreviation such as `10³·m³` when the intended +unit is 1000 cubic meters rather than cubic kilometers. + +CodeGen fails when `Prefixes` is used on a unit that looks like the powered unit itself, such as a unit name starting +with `Square` or `Cubic`, or an abbreviation starting with `m²`, `m³`, `ft³`, or similar powered unit symbols. For +example, generating `Kilo` from `SquareMeter` would produce `km²`, which means one square kilometer (`1e6 m²`), not +one thousand square meters. Generating it from `CubicMeter` would produce `km³`, which means one cubic kilometer +(`1e9 m³`), not one thousand cubic meters. + +This is a naming and abbreviation heuristic, not a dimensional-analysis check. Derived units such as `Watt`, `Joule`, +and `Ohm` have powered SI base dimensions, but can safely use `Prefixes` because their own abbreviations are not +powered unit symbols. They produce unambiguous units such as `MW`, `MJ`, and `MΩ`. + +## Localization object + +Each `Localization` entry configures abbreviations for one culture. + +| Property | Type | Required/default | Description | +|---|---|---|---| +| `Culture` | string | Required | .NET culture name, such as `en-US`, `ru-RU`, or `zh-CN`. | +| `Abbreviations` | array of strings | Empty | Unit symbols and parsing aliases. The first abbreviation is the default used for formatting. Empty is valid for units such as `Ratio.DecimalFraction`. | +| `AbbreviationsForPrefixes` | object | Optional | Explicit abbreviations for selected generated prefixes. Each key is a configured prefix name and each value is a string or array of strings. | + +By default, CodeGen prepends the localized prefix symbol to every abbreviation. Use `AbbreviationsForPrefixes` when +that would produce the wrong symbol or symbol order: + +```json +{ + "Culture": "en-US", + "Abbreviations": [ "∆°C" ], + "AbbreviationsForPrefixes": { + "Milli": "∆m°C" + } +} +``` + +Follow the [abbreviation naming conventions](adding-a-new-unit.md#abbreviation-naming-conventions). Abbreviation +ambiguity across different quantities is allowed, but aliases must not make two units of the same quantity +indistinguishable. + +## Specialized quantity models + +### Linear quantities + +A quantity is linear when both `AffineOffsetType` and `Logarithmic` are omitted. Generated arithmetic operates +directly on converted values. This is the default and applies to quantities such as `Length`, `Mass`, and `Power`. + +### Affine quantities + +An affine quantity represents points on a scale where differences use a separate quantity type. Set +`AffineOffsetType` to that difference quantity: + +```json +"AffineOffsetType": "TemperatureDelta" +``` + +[`Temperature.json`](../Common/UnitDefinitions/Temperature.json) is the current example. Its unit conversions include +scale offsets, while [`TemperatureDelta.json`](../Common/UnitDefinitions/TemperatureDelta.json) defines conversions +between differences without absolute-scale offsets. + +### Logarithmic quantities + +Set `Logarithmic` on a quantity to generate logarithmic arithmetic: + +```json +"Logarithmic": "True", +"LogarithmicScalingFactor": "2" +``` + +The string representation above is retained by existing definitions for compatibility. The values deserialize to a +Boolean and an integer respectively. + +Let `n` be the JSON `LogarithmicScalingFactor`. CodeGen exposes and uses the effective scaling factor: + +```text +S = 10 × n +logarithmic value = S × log10(linear value) +linear value = 10^(logarithmic value / S) +``` + +Therefore: + +| Quantity model | JSON value | Effective factor | Typical relationship | +|---|---:|---:|---| +| Power or generic level | `1` | `10` | `10 × log10(P/P₀)` | +| Field amplitude, such as voltage | `2` | `20` | `20 × log10(V/V₀)` | + +The factor is used when generated operators and `LogarithmicQuantityExtensions` convert values to linear space for +addition, subtraction, sums, and means. It is not automatically applied to `FromUnitToBaseFunc` or +`FromBaseToUnitFunc`; those expressions still define conversion between the logarithmic units and their reference +levels. For example, converting dBm to dBW subtracts 30. + +Current examples are: + +- [`Level.json`](../Common/UnitDefinitions/Level.json) — generic decibels and nepers, factor 1. +- [`PowerRatio.json`](../Common/UnitDefinitions/PowerRatio.json) — dBW and dBm, factor 1. +- [`AmplitudeRatio.json`](../Common/UnitDefinitions/AmplitudeRatio.json) — voltage-referenced levels, factor 2. + +Logarithmic quantities require custom arithmetic test values; see the +[contribution workflow](adding-a-new-unit.md#logarithmic-units). + +## Quantity relations + +Cross-quantity multiplication, division, and inverse relations are not properties of an individual quantity +definition. They are declared separately in [`Common/UnitRelations.json`](../Common/UnitRelations.json). + +Each entry has this form: + +```text +ResultQuantity.ResultUnit = LeftQuantity.LeftUnit * RightQuantity.RightUnit +``` + +For example: + +```json +"Force.Newton = Mass.Kilogram * Acceleration.MeterPerSecondSquared" +``` + +CodeGen infers commutative multiplication and corresponding division operators. Append `-- NoInferredDivision` when +the inferred division would be ambiguous. Use `1` as the result for inverse relationships and `double` for a unitless +numeric operand. + +## Internal and reserved properties + +The deserialization model contains two unit properties that are not used by normal definitions: + +- `SkipConversionGeneration` defaults to `false` and suppresses generated convenience conversion members, numeric + extensions, and related tests. No current unit definition uses it. +- `AllowAbbreviationLookup` defaults to `true`, but the current generators do not consume it. Setting it has no + observable effect. + +Do not use these properties in contributor definitions without a corresponding CodeGen change and tests. +`Quantity.Relations` is also internal generated state populated from `Common/UnitRelations.json`, not a property to +set in a quantity definition. + +Some existing definitions contain historical properties such as `XmlDoc`, `XmlDocsRemarks`, `BaseType`, or +`OmitExtensionMethod`. The current deserialization model does not recognize them, so they have no effect and must not +be copied into new definitions. + +## Validation workflow + +After changing a definition: + +1. Run `generate-code.bat` or `dotnet run --project CodeGen`. +2. Inspect the generated changes. +3. Add or update independently sourced conversion test values. +4. Run `build.bat` or `dotnet build UnitsNet.slnx`. +5. Run the relevant tests. + +Generated files under `GeneratedCode` must not be edited manually. diff --git a/Docs/serialization.md b/Docs/serialization.md index 34e1be5482..eeb2a6f2fd 100644 --- a/Docs/serialization.md +++ b/Docs/serialization.md @@ -4,7 +4,7 @@ - [UnitsNet.Serialization.JsonNet with Json.NET (Newtonsoft)](#unitsnetserializationjsonnet-with-jsonnet-newtonsoft) - [DataContractSerializer for XML](#datacontractserializer-for-xml) - [DataContractJsonSerializer for JSON (not recommended)](#datacontractjsonserializer-for-json-not-recommended) -- [System.Text.Json (not yet implemented)](#systemtextjson-not-yet-implemented) +- [UnitsNet.Serialization.SystemTextJson](#unitsnetserializationsystemtextjson) - [Protobuf and other `[DataContract]` compatible serializers](#protobuf-and-other-datacontract-compatible-serializers) - [Backwards compatibility](#backwards-compatibility) @@ -81,14 +81,49 @@ If you need to support deserializing into properties/fields of type `IComparable jsonSerializerSettings.Converters.Add(new UnitsNetIComparableJsonConverter()); ``` +### Choosing a `QuantityValue` format + +`AbbreviatedUnitsConverter` uses `DecimalPrecision` when writing and `ExactNumber` when reading by default. Configure the +value representation explicitly when exact round-tripping or compatibility with an existing `double`-based payload is +required: + +```c# +var valueFormats = new QuantityValueFormatOptions( + QuantityValueSerializationFormat.RoundTrip, + QuantityValueDeserializationFormat.RoundTrip); + +jsonSerializerSettings.Converters.Add(new AbbreviatedUnitsConverter(valueFormats)); +``` + +Available serialization formats are decimal precision (up to 29 significant digits), double precision, exact +round-tripping and a custom converter. `ExactNumber` reads every digit of a JSON number directly into its numeric +`QuantityValue`; it does not retain the original spelling of the token. Deserialization can alternatively recover +conventional rounded `double` values, read the round-trip representation or use a custom converter. + ## DataContractSerializer for XML All quantities and the `IQuantity` interface have `[DataContract]` annotations and can be serialized by the built-in XML [DataContractSerializer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer). +Because `QuantityValue` is fraction-backed, configure the supplied surrogate provider to avoid exposing the internal +`BigInteger` representation of its numerator and denominator: + +```c# +using System.Runtime.Serialization; +using UnitsNet.Serialization; + +var serializer = new DataContractSerializer(typeof(Power)); +serializer.SetSerializationSurrogateProvider(QuantityValueSurrogateSerializationProvider.Instance); +``` + +The compact representation stores the exact numerator and denominator: + ```xml - 1.20 + + 12 + 10 + Milliwatt ``` @@ -111,7 +146,10 @@ new Foo { Quantity = new Information(1.20m, InformationUnit.Exabyte) }; - 1.20 + + 12 + 10 + Exabyte @@ -134,11 +172,44 @@ Schema: } ``` -## System.Text.Json (not yet implemented) +## UnitsNet.Serialization.SystemTextJson + +Install the `UnitsNet.Serialization.SystemTextJson` package and register converters for the value, unit and quantity +representations you want. For concrete quantity types, this example writes readable decimal values and unit +abbreviations: + +```c# +using System.Text.Json; +using UnitsNet.Serialization.SystemTextJson; +using UnitsNet.Serialization.SystemTextJson.Unit; +using UnitsNet.Serialization.SystemTextJson.Value; + +var options = new JsonSerializerOptions(); +options.Converters.Add(new QuantityValueDecimalNotationConverter()); +options.Converters.Add(new AbbreviatedUnitConverter()); +options.Converters.Add(new JsonQuantityConverter()); + +string json = JsonSerializer.Serialize(Mass.FromGrams(4.2), options); +Mass mass = JsonSerializer.Deserialize(json, options); +// {"Value":4.2,"Unit":"g"} +``` + +To serialize properties declared as `IQuantity`, use an interface converter. The payload includes the quantity type so it +can be reconstructed: + +```c# +var options = new JsonSerializerOptions(); +options.Converters.Add(new QuantityValueDecimalNotationConverter()); +options.Converters.Add(new AbbreviatedInterfaceQuantityWithAvailableValueConverter()); + +string json = JsonSerializer.Serialize(Length.FromMeters(10), options); +IQuantity quantity = JsonSerializer.Deserialize(json, options); +// {"Value":10,"Unit":"m","Type":"Length"} +``` -See -- [WIP: Add serialization support for System.Text.Json #905](https://github.com/angularsen/UnitsNet/pull/905) -- [Add serialization support for System.Text.Json (Attempt #2) #966](https://github.com/angularsen/UnitsNet/pull/966) +For exact round-tripping, use `QuantityValueMixedNotationConverter`. It emits finite values as decimal numbers and +non-terminating values such as one third as fractional strings. Other converters provide fractional-object, decimal and +`double` representations. ## Protobuf and other `[DataContract]` compatible serializers diff --git a/Docs/string-formatting.md b/Docs/string-formatting.md index 9f6ce8ec20..2eb940bf29 100644 --- a/Docs/string-formatting.md +++ b/Docs/string-formatting.md @@ -1,51 +1,65 @@ -# String Formatting +# String formatting -## Common examples +Quantity formatting applies a .NET numeric format string to the value and appends the localized +abbreviation of the quantity's current unit. -Assuming computer running with US English culture. -```c# -var length = Length.FromCentimeters(3.14159265358979); +```csharp +Length length = Length.FromCentimeters(Math.PI); -// Typical formats -length.ToString(); // 3.14 cm -length.ToString("s4"); // 3.1416 cm +length.ToString(); // 3.141592653589793 cm +length.ToString("F2", CultureInfo.InvariantCulture); // 3.14 cm +$"Length: {length:N3}"; // Length: 3.142 cm with an en-US current culture +``` + +The parameterless overload uses the general (`G`) numeric format. Formatting honors the supplied +`IFormatProvider`, or `CultureInfo.CurrentCulture` when none is supplied. -// Localized -length.ToString(new CultureInfo("nb-NO")); // 3,14 cm -length.ToString(new CultureInfo("ru-RU")); // 3,14 sm (Cyrillic) +```csharp +length.ToString(CultureInfo.GetCultureInfo("nb-NO")); // 3,141592653589793 cm +length.ToString(CultureInfo.GetCultureInfo("ru-RU")); // 3,141592653589793 см +``` -// Converted -length.As(LengthUnit.Meters).ToString(); // 0.13 m +Convert the quantity before formatting when a different unit is required: -// Use .NET's built-in formatting methods -Console.WriteLine("Length is {0:v} {0:a}", l); // "Length is 3.14159265358979 ft" -string.Format("Length is {0:v} {0:a}", l); // "Length is 3.14159265358979 ft" -$"Length is {l:v} {l:a}"; // "Length is 3.14159265358979 ft" +```csharp +Length meters = length.ToUnit(LengthUnit.Meter); +string text = meters.ToString("G3", CultureInfo.InvariantCulture); // 0.0314 m ``` -## Standard Quantity Format Strings +## Numeric formats -| Format specifier | Description | Examples | -|------------------|-------------|---------| -| "g" | General quantity pattern. Equivalent to parameterless `ToString()`. Rounds to 2 significant digits after the radix. | `Length.FromFeet(Math.PI).ToString("g")` -> 3.14 ft | -| `f`, `f2`, ... `e`, `e3`, ... `r` `#.0` `00000.0` | [Standard numeric formatting](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#standard-format-specifiers) of value, with unit appended. | `Length.FromFeet(Math.PI).ToString("f")` -> 3.140 ft, `Length.FromFeet(Math.PI).ToString("f1")` -> 3.1 ft, `Length.FromFeet(Math.PI).ToString("r")` -> 3.141592653589793 ft, `Length.FromFeet(Math.PI).ToString("e2")` -> 3.14e+000 ft, `Length.FromFeet(Math.PI).ToString("#.0")` -> 3.1 ft, `Length.FromFeet(Math.PI).ToString("00.0")` -> 003.1 ft | -| "aXX" | Unit abbreviation pattern. If more than one abbreviation is defined for the unit, then XX specifies the zero-indexed position in the array of abbreviations. XX defaults to 0. If the position is not found, `System.FormatException` is thrown. | `Length.FromFeet(Math.PI).ToString("a")` -> ft, `Length.FromFeet(Math.PI).ToString("a0")` -> ft, `Length.FromFeet(Math.PI).ToString("a1")` -> ', `Length.FromFeet(Math.PI).ToString("a2")` -> prime symbol, `Length.FromFeet(Math.PI).ToString("a3")` -> System.FormatException | -| "q" | Quantity name pattern. Outputs the corresponding QuantityType enum name. | `Length.FromFeet(Math.PI).ToString("q")` -> Length, `Mass.FromTonnes(Math.PI).ToString("u")` -> Mass | -| "u" | Unit name pattern. Each quantity type has a corresponding unit enum, such as `Length` quantity having `LengthUnit` unit enum with values `Meter`, `Centimeter` etc. This pattern outputs the unit enum name. | `Length.FromFeet(Math.PI).ToString("u")` -> Foot, `Mass.FromTonnes(Math.PI).ToString("u")` -> Tonne | +UnitsNet accepts standard and custom .NET numeric formats, including: -There are three different overloads of the ToString() method to provide a string representation of a value and its units. +| Intent | Format | Example | +|---|---|---| +| General notation with a precision | `G3` | `3.14 cm` | +| Fixed decimal places | `F2` | `3.14 cm` | +| Grouped number with fixed decimals | `N2` | `1,234.50 cm` | +| Scientific notation | `E2` | `3.14E+000 cm` | +| Up to two decimal places | `0.##` | `3.14 cm` | +| Grouped with up to two decimal places | `#,##0.##` | `1,234.5 cm` | -## Number Formatting +Currency (`C`) and percent (`P`) formats are rejected because adding currency or percent symbols to +a physical quantity is misleading. -For "g" pattern (or if no pattern is specified), the number will be formatted with scientific notation for very small or very large values to increase readability. We did not find .NET's default behavior to work well for this so we created our own rules. +See [.NET standard numeric format strings](https://learn.microsoft.com/dotnet/standard/base-types/standard-numeric-format-strings) +and [.NET custom numeric format strings](https://learn.microsoft.com/dotnet/standard/base-types/custom-numeric-format-strings) +for the complete syntax. -| Interval | Format | Examples | -|-----------|-----------|-------------| -| `(-inf <= x < 1e-03]` | scientific notation | 1e-04; 2.13e-05 | -| `[1e-03 <= x < 1e+03]` | fixed point notation | 0.001; 0.01; 100 | -| `[1e+03 <= x < 1e+06]` | fixed point notation with digit grouping | 1,000; 10,000; 100,000 | -| `[1e+06 <= x <= +inf)` | scientific notation | 1.1e+06; 3.14e+07 | +## Unit abbreviations -The symbols used for digit grouping and radix point are culture-sensitive. The above examples use `CultureInfo.InvariantCulture`. +Use the generated quantity API to get the primary localized abbreviation explicitly: -For more examples, refer to the unit tests in [UnitsNet/UnitFormatter.cs](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/UnitFormatter.cs). +```csharp +string abbreviation = Length.GetAbbreviation(LengthUnit.Foot); +string localized = Length.GetAbbreviation(LengthUnit.Meter, CultureInfo.GetCultureInfo("ru-RU")); +``` + +Use the configured abbreviation cache when every accepted abbreviation is needed. This includes +runtime customizations made through `UnitsNetSetup`: + +```csharp +IReadOnlyList abbreviations = UnitsNetSetup.Default.UnitAbbreviations + .GetUnitAbbreviations(LengthUnit.Foot, CultureInfo.InvariantCulture); +// "ft", "'", "′" +``` diff --git a/Docs/upgrading-from-5.x-to-6.x.md b/Docs/upgrading-from-5.x-to-6.x.md index feef675caf..7d36369790 100644 --- a/Docs/upgrading-from-5.x-to-6.x.md +++ b/Docs/upgrading-from-5.x-to-6.x.md @@ -4,31 +4,39 @@ Before upgrading to a new major version, first upgrade to the latest minor versi ## Summary of changes in v6 -The biggest change is removing support for `decimal` quantities and converting `Power`, `Information`, `BitRate` from `decimal` to `double`. -The value holder type `QuantityValue` is replaced by `double`. +The main change is that all quantities now use the fraction-backed `QuantityValue` type instead of `double` or `decimal`. +Unit definitions are also generated as exact conversion expressions. This preserves exact relationships such as inches to +centimeters and avoids accumulating floating-point error through intermediate conversions. -The motivation was to remove a lot of complexity in the code base. Decimal was initially added for precision issues, but this was later fixed by storing both value and unit. Only `Information` still had any real benefit from `decimal`, to better represent `Bit` as an integer type and avoid rounding errors. - -If there is still enough demand for representing bits as an integer or avoiding rounding errors in `Information` and other quantities, then we can approach that in a simpler way like `TimeSpan.Ticks` vs `TimeSpan.TotalSeconds`. - -If there is sufficient demand for supporting any number type like `float`, `decimal` or even `Half`, then a more holistic approach is required using generics, which brings its own challenges. +`QuantityValue` interoperates with the standard numeric types: numeric values convert implicitly to `QuantityValue`, and +`QuantityValue` converts implicitly to `double`. Code that explicitly declares `double` variables will therefore often +continue to compile, while code using `var` will now infer `QuantityValue`. Unit conversions and arithmetic remain exact +while values stay as `QuantityValue`; converting a value to `double` is the boundary where values that cannot be represented +as binary floating point lose precision. ## New -- Allow `NaN`, `Inf` values for quantities with `double` value type #1289 +- Allow `NaN` and infinity values in quantities #1289 +- Exact fractional values, arithmetic and generated unit conversions with `QuantityValue` #1544 +- `UnitsNet.Serialization.SystemTextJson`, with configurable quantity, unit and value converters #1544 +- Annotate numeric format-string parameters with `StringSyntaxAttribute.NumericFormat` so supported IDEs can provide + syntax assistance ## Breaking changes ### Binary incompatible -- Remove `decimal` support in quantities #1359, everything is now `double` - - Convert quantities `Power`, `Information`, `BitRate` from `decimal` to `double` #1195, #1353 - - Remove `QuantityValue`, replaced with `double` +- Rework `QuantityValue` as a fraction-backed number and use it for every quantity value #1544 + - `IQuantity.Value`, generated unit properties, `.As()`, ratio methods and related conversion APIs now return `QuantityValue` + - Quantity factories and constructors now accept `QuantityValue`; standard numeric arguments continue to work through implicit conversions + - `Power`, `Information` and `BitRate` no longer have a separate `decimal` value type +- Rework `UnitConverter` configuration and conversion APIs around exact generated conversion expressions #1544 - Remove `TValueType` from interfaces - Remove `IQuantity` - Remove `IValueQuantity` - Change `IQuantity` to `IQuantity` - Change `IArithmeticQuantity` to `IArithmeticQuantity` +- Move `IQuantity.As()` and `IQuantity.ToUnit()` from the quantity interfaces to `QuantityExtensions` #1696 - Remove obsolete units #1372 - `CoefficientOfThermalExpansion.InverseKelvin`, `InverseDegreeCelsius`, `InverseDegreeFahrenheit` - `HeatTransferCoefficient.BtuPerSquareFootDegreeFahrenheit` @@ -39,14 +47,77 @@ If there is sufficient demand for supporting any number type like `float`, `deci - `FuelEfficiency.KilometersPerLiter` - `Speed.MetersPerMinute` - Moved 29 operator overloads for multiply or division to another type ([details](https://github.com/angularsen/UnitsNet/pull/1329#discussion_r1451794868)), e.g. `Energy op_Multiply(Duration, Power)` moved from `Power` to `Duration` #1329 +- Rename or remove ambiguous prefixed cubic units #1617, #1645, #1700 + - `SpecificVolumeUnit.MillicubicMeterPerKilogram` -> `SpecificVolumeUnit.CubicMillimeterPerKilogram` + - Remove `VolumeUnit.HectocubicMeter`; use `VolumeUnit.CubicMeter` for 100 m³ values + - `VolumeUnit.KilocubicMeter` -> `VolumeUnit.ThousandCubicMeter` + - `VolumeUnit.HectocubicFoot` -> `VolumeUnit.HundredCubicFoot` + - `VolumeUnit.KilocubicFoot` -> `VolumeUnit.ThousandCubicFoot` + - `VolumeUnit.MegacubicFoot` -> `VolumeUnit.MillionCubicFoot` ### Source incompatible - `IQuantity.UnitInfo` is now a interface default member on .NET5+, and may compete with any custom property implemented in third party quantities #1649 +- Custom quantity implementations must expose `QuantityValue` from `IQuantity.Value` and accept it in their quantity factories. #1544 +- Expressions inferred with `var`, such as `var value = quantity.Value` or `var value = quantity.As(unit)`, now have type `QuantityValue`. Cast to `double` when a floating-point result is specifically required. #1544 +- Custom quantities that explicitly implement `IQuantity.As()`, `IQuantity.ToUnit()`, `IQuantity.As()` or `IQuantity.ToUnit()` must remove those explicit interface implementations. The methods may remain as ordinary members if they are also part of the custom quantity's public API. #1696 ### Behavioral change -None. +- Complete the removal of proprietary quantity format strings. Quantity formatting now accepts .NET numeric format + strings only; see [String formatting](string-formatting.md). + - Replace `A`, `A0`, `A1`, ... with a generated `GetAbbreviation()` method or + `UnitAbbreviationsCache.GetUnitAbbreviations()`. + - Replace `S`, `S2`, ... with an explicit numeric format such as `G3`, `F2`, `N2`, `E2`, or `0.##`. + - Replace `U`, `V`, and `Q` with the `Unit` property, the `Value` property, and static quantity metadata such as + `Length.Info.Name`, respectively. +- Exact rational conversions and arithmetic may produce results that differ from the previous `double` implementation in the least significant digits. Precision can be lost when a `QuantityValue` is converted to `double`; perform that conversion only at boundaries where floating-point behavior is required. #1544 +- Calls to `.As()` and `.ToUnit()` through an `IQuantity` or `IQuantity` reference now use the `QuantityExtensions` methods and `UnitConverter.Default`. They no longer dispatch to type-specific methods defined by a custom quantity. Custom quantities that need these calls to support conversion must register their conversion functions with `UnitConverter.Default`. #1696 +- Calling these extension methods with an incompatible unit type now throws `UnitNotFoundException` instead of `ArgumentException`. Code that catches `ArgumentException` around interface-based conversions may need to be updated. #1696 +- `SpecificVolume` abbreviation `mm³/kg` now parses as true cubic millimeters per kilogram (`1e-9 m³/kg`) instead of millesimal cubic meters per kilogram. +- `Volume` abbreviations `hm³` and `km³` now parse unambiguously as true cubic hectometers and cubic kilometers. +- Thousand cubic meters now formats as `10³·m³`, with `kcm` and `Kcm` as parsing aliases. +- Count-style cubic-foot volume units now format as `Ccf`, `Mcf`, and `MMcf`. + +### Serialization + +The serialized form of a quantity can change because its `Value` is now a `QuantityValue`. Review persisted payloads and +choose an explicit value format when compatibility matters: + +- `UnitsNet.Serialization.JsonNet` uses `DecimalPrecision` when writing and `ExactNumber` when reading by default. It also + supports double precision, exact round-tripping and custom value converters through `QuantityValueFormatOptions`. +- The new `UnitsNet.Serialization.SystemTextJson` package provides converters for concrete quantities, `IQuantity`, units + and several `QuantityValue` representations. +- The default `DataContractSerializer` representation exposes the internal `BigInteger` fields of the fraction's + numerator and denominator. Use `QuantityValueSurrogateSerializationProvider` for a compact, stable numerator and + denominator representation in XML. +- `DataContractJsonSerializer` cannot apply that surrogate provider to nested `QuantityValue` instances due to + [dotnet/runtime#100553](https://github.com/dotnet/runtime/issues/100553). Use the Json.NET or System.Text.Json package + instead. + +See [Serialization](serialization.md) for examples. + +### Ambiguous prefixed cubic units + +Previous versions generated some cubic units from metric prefixes where the prefix applied to the generated unit name, +but users would reasonably read the abbreviation as applying before cubing the length unit. For example, `km³` should +mean `(1000 m)³`, not `1000 m³`. + +In v6, `hm³` and `km³` are reserved for the existing `CubicHectometer` and `CubicKilometer` units. The old +`HectocubicMeter` API was removed. For 100 cubic meters, use `Volume.FromCubicMeters(100)` or +`volume.As(VolumeUnit.CubicMeter)` instead. + +The old `KilocubicMeter` API represented 1000 cubic meters, which is a real count-style unit in some domains. It was +renamed to `ThousandCubicMeter` and formats as `10³·m³`. It also accepts `kcm` and `Kcm` as aliases. + +For cubic feet, the generated prefix names were renamed to count-style names: + +- `HectocubicFoot` -> `HundredCubicFoot`, default abbreviation `Ccf` +- `KilocubicFoot` -> `ThousandCubicFoot`, default abbreviation `Mcf` +- `MegacubicFoot` -> `MillionCubicFoot`, default abbreviation `MMcf` + +The old generated cubic-foot abbreviations such as `hft³`, `kft³`, and `Mft³` are still accepted as aliases where they +do not conflict with another unit. ### Description of different kinds of incompatible changes diff --git a/GITHUB_ACTIONS_MIGRATION_README.md b/GITHUB_ACTIONS_MIGRATION_README.md deleted file mode 100644 index 9d19216a7a..0000000000 --- a/GITHUB_ACTIONS_MIGRATION_README.md +++ /dev/null @@ -1,44 +0,0 @@ -# GitHub Actions Migration - -This PR adds GitHub Actions workflows to run alongside the existing Azure Pipelines configuration. - -## Files to Move - -Due to GitHub App permission restrictions, the workflow files are created in the root directory. Please move these files to `.github/workflows/` after merging: - -1. `github-actions-ci.yml` → `.github/workflows/ci.yml` -2. `github-actions-pr.yml` → `.github/workflows/pr.yml` - -## Key Features - -### CI Workflow (ci.yml) -- Triggers on pushes to master, release/*, and maintenance/* branches -- Builds with .NET nanoFramework support using `nanoframework/nanobuild@v1` action -- Runs tests and uploads coverage to codecov.io -- Publishes NuGet packages to nuget.org (only on master branch) -- Uses Windows runner to match Azure Pipelines configuration - -### PR Workflow (pr.yml) -- Triggers on pull requests to master, release/*, and maintenance/* branches -- Same build and test process as CI workflow -- Publishes test results as PR checks -- Uploads test coverage to codecov.io -- No NuGet publishing for PRs - -## .NET nanoFramework Support - -The key challenge mentioned in the issue was .NET nanoFramework support. This is handled using the `nanoframework/nanobuild@v1` GitHub Action, which is the official GitHub Actions equivalent of the Azure Pipelines `InstallNanoMSBuildComponents@1` task. - -## Migration Notes - -- Both workflows use the existing PowerShell build script (`Build/build.ps1`) with the `-IncludeNanoFramework` flag -- Secrets needed: `CODECOV_TOKEN` and `NUGET_ORG_APIKEY` (should already be configured) -- The workflows are designed to run alongside Azure Pipelines for comparison -- Azure Pipelines configuration files remain unchanged as requested - -## Next Steps - -1. Move the workflow files to `.github/workflows/` -2. Test both workflows to ensure they work correctly -3. Compare results with Azure Pipelines -4. Make any necessary adjustments based on testing \ No newline at end of file diff --git a/NullableAttributes.cs b/NullableAttributes.cs index 78402f17d0..c1b5542a5f 100644 --- a/NullableAttributes.cs +++ b/NullableAttributes.cs @@ -1,4 +1,4 @@ -// Workaround for nullable annotations when multitargeting against netstandard2.0 or .NET versions lower than .NET Core 3.0, which do not support it out of the box. +// Workaround for nullable annotations when multitargeting against netstandard2.0 or .NET versions lower than .NET Core 3.0, which do not support it out of the box. // https://www.meziantou.net/how-to-use-nullable-reference-types-in-dotnet-standard-2-0-and-dotnet-.htm // https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs diff --git a/PerfTests/PerfTest_Startup/Program.cs b/PerfTests/PerfTest_Startup/Program.cs index b54c226ab3..3a1c988469 100644 --- a/PerfTests/PerfTest_Startup/Program.cs +++ b/PerfTests/PerfTest_Startup/Program.cs @@ -1,4 +1,4 @@ -using UnitsNet; +using UnitsNet; using UnitsNet.Units; Console.WriteLine(Power.From(5, PowerUnit.Watt)); diff --git a/PerfTests/PerfTest_Startup_Aot/Program.cs b/PerfTests/PerfTest_Startup_Aot/Program.cs index b54c226ab3..3a1c988469 100644 --- a/PerfTests/PerfTest_Startup_Aot/Program.cs +++ b/PerfTests/PerfTest_Startup_Aot/Program.cs @@ -1,4 +1,4 @@ -using UnitsNet; +using UnitsNet; using UnitsNet.Units; Console.WriteLine(Power.From(5, PowerUnit.Watt)); diff --git a/PerfTests/PerfTest_Startup_v4_144_0/Program.cs b/PerfTests/PerfTest_Startup_v4_144_0/Program.cs index 8bbbc94c42..7a51a29550 100644 --- a/PerfTests/PerfTest_Startup_v4_144_0/Program.cs +++ b/PerfTests/PerfTest_Startup_v4_144_0/Program.cs @@ -1,4 +1,4 @@ -// See https://aka.ms/new-console-template for more information +// See https://aka.ms/new-console-template for more information using UnitsNet; using UnitsNet.Units; diff --git a/PerfTests/PerfTest_Startup_v4_72_0/Program.cs b/PerfTests/PerfTest_Startup_v4_72_0/Program.cs index 8bbbc94c42..7a51a29550 100644 --- a/PerfTests/PerfTest_Startup_v4_72_0/Program.cs +++ b/PerfTests/PerfTest_Startup_v4_72_0/Program.cs @@ -1,4 +1,4 @@ -// See https://aka.ms/new-console-template for more information +// See https://aka.ms/new-console-template for more information using UnitsNet; using UnitsNet.Units; diff --git a/README.md b/README.md index ba3deae87d..a3d6689f2c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -[![Build Status](https://dev.azure.com/unitsnet/Units.NET/_apis/build/status/UnitsNet?branchName=master)](https://dev.azure.com/unitsnet/Units.NET/_build/latest?definitionId=1&branchName=master) +[![CI Build](https://github.com/angularsen/UnitsNet/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/angularsen/UnitsNet/actions/workflows/ci.yml) +[![Pull Requests](https://github.com/angularsen/UnitsNet/actions/workflows/pr.yml/badge.svg)](https://github.com/angularsen/UnitsNet/actions/workflows/pr.yml) +[![.NET Framework 4.8](https://github.com/angularsen/UnitsNet/actions/workflows/net48-compatibility.yml/badge.svg)](https://github.com/angularsen/UnitsNet/actions/workflows/net48-compatibility.yml) [![codecov](https://codecov.io/gh/angularsen/UnitsNet/branch/master/graph/badge.svg)](https://codecov.io/gh/angularsen/UnitsNet) [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) ## Units.NET -Add strongly typed quantities to your code and get merrily on with your life. +Add strongly typed quantities and units to your code and get merrily on with your life. -No more magic constants found on Stack Overflow, no more second-guessing the unit of parameters and variables. +No more magic constants found online or guessing the unit of variables. ### Changes @@ -16,6 +18,8 @@ New units will be backported to `maintenance/v5` until v6 becomes stable. [Upgrading from 5.x to 6.x](https://github.com/angularsen/UnitsNet/wiki/Upgrading-from-5.x-to-6.x)
[Upgrading from 4.x to 5.x](https://github.com/angularsen/UnitsNet/wiki/Upgrading-from-4.x-to-5.x)
+🧪 **Experimental:** Check out [UnitsNet.Modular](UnitsNet.Modular/README.md), which generates only the quantities and units your application needs. + ### Overview * [Overview](#overview) @@ -51,7 +55,6 @@ or go to [NuGet Gallery | UnitsNet](https://www.nuget.org/packages/UnitsNet) for * .NET 8.0 (LTS) * .NET 9.0 (latest stable) * .NET 10.0 (preview) -* [.NET nanoFramework](https://www.nanoframework.net/) ### Extension Packages @@ -460,6 +463,7 @@ Read the wiki on [Serializing to JSON, XML and more](https://github.com/angulars ### Want To Contribute? * [Adding a New Unit](https://github.com/angularsen/UnitsNet/wiki/Adding-a-New-Unit) is fairly easy to do and we are happy to help. +* See the [Quantity and Unit Definition Schema](Docs/quantity-and-unit-definition-schema.md) for a reference to the JSON definition format. * Want a new feature or to report a bug? [Create an issue](https://github.com/angularsen/UnitsNet/issues/new/choose) or start a [discussion](https://github.com/angularsen/UnitsNet/discussions). @@ -472,16 +476,20 @@ Get the same strongly typed units on other platforms, based on the same [unit de | JavaScript /
TypeScript | unitsnet-js | [npm](https://www.npmjs.com/package/unitsnet-js) | [github](https://github.com/haimkastner/unitsnet-js) | @haimkastner | | Python | unitsnet-py | [pypi](https://pypi.org/project/unitsnet-py) | [github](https://github.com/haimkastner/unitsnet-py) | @haimkastner | | Golang | unitsnet-go | [pkg.go.dev](https://pkg.go.dev/github.com/haimkastner/unitsnet-go) | [github](https://github.com/haimkastner/unitsnet-go) | @haimkastner | +| C++ | unitsnet-cpp | [CPM.cmake](https://github.com/JeroenVandezande/unitsnet-cpp#add-with-cpmcmake) | [github](https://github.com/JeroenVandezande/unitsnet-cpp) | @JeroenVandezande | | .NET nanoFramework | nanoFramework.UnitsNet | [nuget](https://www.nuget.org/packages/nanoFramework.UnitsNet.Acceleration/) | [github](https://github.com/nanoframework/nanoFramework.UnitsNet) | @josesimoes | ### Continuous Integration -[Azure DevOps](https://dev.azure.com/unitsnet/Units.NET/) performs the following: +[GitHub Actions](https://github.com/angularsen/UnitsNet/actions) performs the following for v6 on `master`: + +* The [CI workflow](https://github.com/angularsen/UnitsNet/actions/workflows/ci.yml) builds and tests pushes to `master` and publishes packages to [NuGet.org](https://www.nuget.org/packages/UnitsNet) +* The [pull-request workflow](https://github.com/angularsen/UnitsNet/actions/workflows/pr.yml) builds and tests pull requests, with test results and code coverage +* Linux CI builds every shipped target and runs the full test suite with coverage on .NET 10 +* The [.NET Framework 4.8 compatibility workflow](https://github.com/angularsen/UnitsNet/actions/workflows/net48-compatibility.yml) runs the complete test suite against the `netstandard2.0` assemblies on CLR4 for pull requests and pushes to `master` -* Build and test all branches -* Build and test pull requests, notifies on success or error -* Deploy NuGets on master branch, if nuspec versions changed +The [`maintenance/v5`](https://github.com/angularsen/UnitsNet/tree/maintenance/v5) branch remains on Azure DevOps because its nanoFramework build requires Visual Studio MSBuild and the nanoFramework MSBuild components. ### Who are Using UnitsNet? diff --git a/Samples/Directory.Packages.props b/Samples/Directory.Packages.props index cb16f092ef..7f15b25a21 100644 --- a/Samples/Directory.Packages.props +++ b/Samples/Directory.Packages.props @@ -3,11 +3,12 @@ true - + - + + \ No newline at end of file diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml index db543e2d84..3d95351129 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml @@ -1,4 +1,4 @@ - diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml.cs b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml.cs index 9b9bf71e09..5202334f4b 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml.cs +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/App.xaml.cs @@ -1,4 +1,4 @@ -using System.Windows; +using System.Windows; using Prism.Ioc; using WpfMVVMSample.Settings; diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/EnumBindingSource.cs b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/EnumBindingSource.cs index db5b223288..0d4a0e5332 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/EnumBindingSource.cs +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/EnumBindingSource.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Markup; diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs index f7f2806f73..1b98dadf6e 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Converters/UnitToStringConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Globalization; using System.Linq; diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MainWindow.xaml b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MainWindow.xaml index ce9546ce7e..a2102779a6 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MainWindow.xaml +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/MainWindow.xaml @@ -1,4 +1,4 @@ - + net48 WinExe @@ -6,6 +6,8 @@ false true true + Debug;Release;Official + AnyCPU diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/AssemblyInfo.cs b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/AssemblyInfo.cs index 61146f9a9c..9904141bc5 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/AssemblyInfo.cs +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.Designer.cs b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.Designer.cs index b95ae2d722..372d50bc6b 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.Designer.cs +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 diff --git a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.resx b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.resx index af7dbebbac..13b775f1d9 100644 --- a/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.resx +++ b/Samples/MvvmSample.Wpf/MvvmSample.Wpf/Properties/Resources.resx @@ -1,4 +1,4 @@ - + + + + + + + + + \ No newline at end of file diff --git a/Samples/Samples.slnx b/Samples/Samples.slnx index 1fc01829b9..7e83cc458d 100644 --- a/Samples/Samples.slnx +++ b/Samples/Samples.slnx @@ -1,20 +1,18 @@ - - - - + + + - - - + - - + + + - + \ No newline at end of file diff --git a/Samples/UnitConverter.Console/Program.cs b/Samples/UnitConverter.Console/Program.cs index 7ba55aef09..9adfb8e3fd 100644 --- a/Samples/UnitConverter.Console/Program.cs +++ b/Samples/UnitConverter.Console/Program.cs @@ -1,4 +1,5 @@ -using UnitsNet; +using UnitsNet; +using UnitsNet.Units; using static System.Console; using static UnitsNet.Units.LengthUnit; diff --git a/Samples/UnitConverter.Console/UnitConverter.Console.csproj b/Samples/UnitConverter.Console/UnitConverter.Console.csproj index ae8f5130fb..2f3886131f 100644 --- a/Samples/UnitConverter.Console/UnitConverter.Console.csproj +++ b/Samples/UnitConverter.Console/UnitConverter.Console.csproj @@ -5,6 +5,8 @@ net10.0 enable enable + Debug;Release;Official + AnyCPU diff --git a/Samples/UnitConverter.Wpf/README.md b/Samples/UnitConverter.Wpf/README.md index 4f8fe8034a..228257158c 100644 --- a/Samples/UnitConverter.Wpf/README.md +++ b/Samples/UnitConverter.Wpf/README.md @@ -1,4 +1,4 @@ -## Unit Converter WPF Sample App +## Unit Converter WPF Sample App This is a simple sample showing how UnitsNet can be used to create a generic unit converter, using all the quantities and units available in the UnitsNet library. diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf.sln.DotSettings b/Samples/UnitConverter.Wpf/UnitConverter.Wpf.sln.DotSettings index 8939b3c194..077a26e618 100644 --- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf.sln.DotSettings +++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf.sln.DotSettings @@ -1,2 +1,2 @@ - + True \ No newline at end of file diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml index 86dd63140b..b277545a44 100644 --- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml +++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml @@ -1,4 +1,4 @@ - diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml.cs b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml.cs index 7244904df2..125b5d7779 100644 --- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml.cs +++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/App.xaml.cs @@ -1,4 +1,4 @@ -using System.Windows; +using System.Windows; namespace UnitsNet.Samples.UnitConverter.Wpf { diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs index c2ed86fb67..81817ad7ac 100644 --- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs +++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/IMainWindowVm.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Input; using UnitsNet.Samples.UnitConverter.Wpf.Properties; @@ -23,8 +23,8 @@ public interface IMainWindowVm : INotifyPropertyChanged string FromHeader { get; } string ToHeader { get; } - double FromValue { get; set; } - double ToValue { get; } + QuantityValue FromValue { get; set; } + QuantityValue ToValue { get; } ICommand SwapCommand { get; } } } diff --git a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml index 81d8491452..fa493adecf 100644 --- a/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml +++ b/Samples/UnitConverter.Wpf/UnitConverter.Wpf/MainWindow.xaml @@ -1,4 +1,4 @@ - - @@ -32,7 +29,7 @@ SelectionChanged="Selector_OnSelectionChanged" /> - + - + - + + + + + + + Text="{Binding FromValue, StringFormat=G35, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> - +