{"id":31849049,"url":"https://github.com/bogoware/moneta","last_synced_at":"2026-05-15T22:02:36.749Z","repository":{"id":218782355,"uuid":"717622811","full_name":"bogoware/Moneta","owner":"bogoware","description":"A .NET Library to support secure monetary calculations","archived":false,"fork":false,"pushed_at":"2025-03-12T16:38:45.000Z","size":172,"stargazers_count":0,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"rel/prod","last_synced_at":"2025-08-30T16:31:55.945Z","etag":null,"topics":["currency","currency-converter","currency-exchange","library","moneta","monetary","money","nuget","package","utils"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bogoware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-11-12T03:04:06.000Z","updated_at":"2025-03-12T16:38:49.000Z","dependencies_parsed_at":"2024-03-10T00:45:17.854Z","dependency_job_id":null,"html_url":"https://github.com/bogoware/Moneta","commit_stats":null,"previous_names":["bogoware/moneta"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/bogoware/Moneta","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bogoware%2FMoneta","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bogoware%2FMoneta/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bogoware%2FMoneta/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bogoware%2FMoneta/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bogoware","download_url":"https://codeload.github.com/bogoware/Moneta/tar.gz/refs/heads/rel/prod","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bogoware%2FMoneta/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279005030,"owners_count":26083827,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["currency","currency-converter","currency-exchange","library","moneta","monetary","money","nuget","package","utils"],"created_at":"2025-10-12T10:53:07.910Z","updated_at":"2025-10-12T10:54:02.391Z","avatar_url":"https://github.com/bogoware.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Moneta\n\n![Nuget](https://img.shields.io/nuget/dt/Bogoware.Moneta?logo=nuget\u0026style=plastic) ![Nuget](https://img.shields.io/nuget/v/Bogoware.Moneta?style=plastic)\n\nMoneta is a library designed to support monetary calculations in a secure manner.\n\n## TL;DR\n\nThe following simple snippet demonstrates how Moneta can help you writing safe monetary code also in very simple scenarios.\n\n```csharp\npublic static void BadCode()\n{\n    using var moneta = new MonetaContext();\n    var unitPrice = moneta.Dollar(1.12m);\n    var quantity = 12.43424m;\n    var finalPrice = unitPrice * quantity;\n    \n    Console.WriteLine($\"Unit price: {unitPrice}\");\n    Console.WriteLine($\"Quantity: {quantity}\");\n    Console.WriteLine($\"Final price: {finalPrice}\");\n    \n} // an exception will be thrown because rounding errors were unnoticed\n\npublic static void GoodCode()\n{\n    using var moneta = new MonetaContext();\n    var unitPrice = moneta.Dollar(1.12m);\n    var quantity = 12.43424m;\n    var finalPrice = unitPrice * quantity;\n    \n    Console.WriteLine($\"Unit price: {unitPrice}\");\n    Console.WriteLine($\"Quantity: {quantity}\");\n    Console.WriteLine($\"Final price: {finalPrice}\");\n\n    if (moneta.HasRoundingErrors)\n    {\n        // Handle rounding errors as you prefer\n        moneta.ClearRoundingErrors();\n    }\n}\n```\n\n### Summary\n\n* [Moneta Context](#moneta-context)\n* [The Principle of Monetary Value Conservation](#the-principle-of-monetary-value-conservation)\n  * [Rounding Error Detection](#rounding-error-detection)\n* [Money](#money)\n* [Supported Operations](#supported-operations)\n  * [Split](#split)\n  * [RoundOff](#roundoff)\n  * [Apply](#apply)\n  * [Add](#add)\n  * [Subtract](#subtract)\n  * [Multiply](#multiply)\n  * [Divide](#divide)\n  * [Negate](#negate)\n  * [CompareTo](#compareto)\n  * [Binary Operators (+, -, *, /, \u003c, \u003c=, …)](#operators)\n* [Currency](#currency)\n* [Rounding Error Detection](#rounding-error-detection)\n* [Safe and Unsafe Operations](#safe-and-unsafe-operations)\n* [Currency System](#currency-system)\n* [Currency Providers](#currency-providers)\n* [Exchange Rate Conversion](#exchange-rate-conversion) TODO\n* [Exchange Rate Providers](#exchange-rate-providers) TODO\n* [Samples](#samples) TODO\n* [Dependency Injection](#dependency-injection) TODO\n\n## Concepts and Key Features\n\n### Moneta Context\n\nA `MonetaContext` sets the boundaries for safe and coherent monetary operations, ensuring that no rounding errors go unnoticed.\n\nA `MonetaContext` defines the following:\n* The default `ICurrency` for creating new `Money` instances. For example, if you exclusively deal with EUR, you can set EUR as the default currency, making the creation of new `Money` instances more straightforward.\n* The `ICurrencyProvider` used to resolve currencies by code, enabling you to resolve currencies from a database or a web service.\n* The default `RoundingMode` for monetary operations.\n* The decimal precision used to detect rounding errors (see `RoundingErrorDecimals`). By default, all internal operations are rounded to 8 decimal places, but you can change this value up to 28 decimal places.\n* A log of operations that have resulted in unnoticed rounding errors (according to the `RoundingErrorDecimals` value).\n\n### The Monetary Value Conservation Principle\n\nIn Moneta holds the «Monetary Value Conservation Principle»: no monetary value is created or lost *unnoticed* during the lifetime of a `MonetaContext`.\n\nMoneta will provide means to keep track of any monetary value created or lost during the lifetime of a `MonetaContext`\nand make it available to the user who can decide how to handle it.\n\nThere are basically two main causes of monetary value creation or loss:\n* Split or RoundOff Operations: which can produce an unallocated part, both positive or negative depending by the `RoundingMode` used.\n* Floating Point Operations with floating point numbers more precise than the `Currency` of the `Money` involved in the operation.\n\n#### Rounding Error Detection\n\nA *rounding error* occurs when an operation cannot be performed without losing precision.\nThese errors are influenced not only by the *values* involved in the operation but also by the `RoundingMode` and\nthe `MonetaContext.RoundingErrorDecimals` that is used by the `MonetaContext` to detect rounding errors.\n\n\u003e [!IMPORTANT]\n\u003e `MonetaContext` will prevent you to create any `Money` that uses a `Currency` with a number of `DecimalPlaces` greather than the `RoundingErrorDecimals` value.\n\n\u003e [!NOTE]\n\u003e Using a `RoundingErrorDecimals` equals to the `Currency` with the highest number of `DecimalPlaces` involved in your calculus will prevent any rounding error to get noticed.\n\nEvery result of an operation involving a floating point operand is rounded to the `MonetaContext.RoundingErrorDecimals`\nbefore the monetary value is computed.\nThi value is then rounded accordingly to the decimals required by the `Currency` of the `Money` involved.\nThe difference between the two values is the rounding error.\n \nEvery safe operation will return a `decimal` error value or, in the case of the [Split](#split-operation) and [RoundOff](#roundoff-operation), a `Money` value representing the amount of value unallocated.\nUnsafe operations, instead, will keep track of the operations and the errors occurred.\n\nSchematically, let's assume that:\n* `•` is the operation performed\n* `M` is the `Money` involved in the operation\n* `V` is the value (`decimal`, `double` or `float`) involved in the operation\n* `R` is the `Money` returned by the operation\n* `E` is the `error` returned by the operation\n\nthen the following equation holds for the algebraic operations:\n\n```\nM • V = R + E\n```\n\nMore precisely, if we indicate with $M$ the money value and with $value$ the value involved in the operation and $error$ the rounding error, then the following equation holds:\n\n```math\n\\Big\\| M \\bullet  value \\Big\\|^{RoundingMode}_{RoundingErrorDecimals} = R+error\n```\n\nSimilarly, in case of `Split`, if we indicate with $M_0$, … $M_{n-1}$ the $n$ parts\nof the original value $M$ and with $U$ the unallocated part, then the following equation holds:\n\n```math\n\\sum_{i=0}^n M_i = M + U\n```\n\n\u003e [!NOTE]\n\u003e $error$ and $U$ are positive in case of monetary value lost and negative in case of monetary value created.\n \n\u003e [!NOTE]\n\u003e The unallocated part $U$ of a `Split` operation can be positive or negative depending by the rounding algorithm.\n\n\u003e  [!IMPORTANT]\n\u003e `error` is always a `decimal` value with `MonetaContext.RoundingErrorDecimals` decimals at most.\n\nFor example, if you perform the operation `1.00 EUR + 0.1234` with rounding mode `ToZero` or `ToEven` you will get `1.12 EUR` with a rounding error of `0.0034`,\nwhich is the amount of monetary value lost during the operation.\nBut if you perform the same operation with rounding mode `ToPositiveInfinity` you will get `1.13 EUR` with a rounding error of `-0.0066`,\nwhich is the amount of monetary value created during the operation.\n\nIt's up to you to decide which treatment to apply to the rounding errors depending by your domain requirements.\n\n### Moneta API Design\n\nMoneta main design goal is to support safe monetary calculations through a fluent algebraic API.\n\nFor every supported operation, Moneta provides two set of overloads:\n* *safe* overloads that returns the rounding error or, in the case of the `Spilt` operations, the unallocated part\n* *unsafe* overloads that doesn't return the created or lost monetary value and relies on the `MonetaContext` to keep track of it.\n\nThe `error` or `unallocated` part returned by the safe overloads represents a quantity of the monetary value that has been created or lost during the operation.\n\n### Money\n\n`Money` is Moneta's type for representing monetary values. It consists of a `decimal` value associated with a `Currency`.\n\nThe supported operations are:\n* `Split`: will split a `Money` into a list of `Money` instances according to the specified `RoundingMode` and number of parts or weights.\n* `Apply`: will apply a function to the `Money` value and return a new `Money` instance. There are many variants that accept different kind of functions suitable to transform the `Money` amount and/or the `Currency`.\n* `Add`, `Subtract`, `Multiply`, `Divide`: will perform the corresponding operation between two `Money` instances or a `Money` instance and a number, returning a new `Money` instance with the same `Currency`.\n* `Negate`: will negate the `Money` value and return a new `Money` instance with the same `Currency`.\n* `CompareTo`: will compare the `Money` value with another `Money` instance or a number. If the `Money` instances have different `Currencies`, and the `MonetaContext` has an `IExchangeRateProvider`, the `Money` instances will be converted to the same `Currency` before the comparison, otherwise an exception will be thrown.\n\nAnd of course you can also use basic operators such as `+`, `-`, `*`, `/`, `==`, `!=`, `\u003e`, `\u003e=`, `\u003c`, `\u003c=`. All the binary operators between a `Money` instance and a number are unsafe operations (see [Safe and Unsafe Operations](#safe-and-unsafe-operations)).\n\n### Currency\n\nIn Moneta, a Currency is any instance of `Currency\u003cTSelf\u003e`. There are no strict constraints on what qualifies as a valid currency; you are free to introduce your own currency as long as it fits your domain. The only requirement is that currencies with the same `Code` are treated as equivalent.\n\n#### Currency Decimal Places\n\nAn important characteristic of a currency is the number of decimal places it supports. When you perform a monetary operation, the result is rounded based on the currency's decimal precision, potentially resulting in rounding errors.\n\n##### Rounding Errors in Split Operations\n\nRounding errors in split operations occur when you attempt to divide a `Money` into parts that cannot be divided fairly according to the `RoundingMode` used. For example, if you try to divide 1.00 EUR into 3 parts, the result will be 0.33 EUR with a rounding error of 0.01 EUR. Similar situations arise when you perform a *weighted* split operation using floating point weights.\n\n##### Rounding Errors in Algebraic Operations\n\nRounding errors in algebraic operations occur when you perform an operation between a `Money` and a floating point number with a decimal part that exceeds the decimal places supported by the target `Currency`. For instance, if you try to add 1.12345678 EUR to 1.12 EUR, the result will be 2.24 EUR with a rounding error of 0.00345678 EUR.\n\n#### Common Currencies\nThe Moneta core is equipped with common currencies and provides extension methods to the `MonetaContext`. These methods aim to simplify the most common use cases, eliminating the need to inject an `ICurrencyProvider`.\n\nFor example, if you operate with standard EUR and USD you can write something like this:\n\n```csharp\nusing var moneta = new MonetaContext();\nvar bucks = moneta.Dollars(100);\nvar euros = moneta.Euros(100);\nvar pounds = moneta.PoundingSterling(100);\nvar yens = moneta.Yen(100);\nvar yuans = moneta.Yuan(100);\n```\n\n### Safe and Unsafe Operations\n\nMoneta's API offers two types of operations: *safe* and *unsafe* operations.\n\n*Safe operations* are operations that return the `error` of the operation to the caller and relieve the context from tracking rounding errors. The caller is responsible for handling the error in line with domain requirements.\n\nIn contrast, *unsafe operations* do not return the `error` of the operation, and the context is responsible for keeping track of rounding errors. The caller must check the context for any rounding errors and address them accordingly.\n\nIn particular:\n* All binary operations between a `Money` value and a floating point number are unsafe operations.\n* All `Money.Map` operations are unsafe operations.\n\n### Supported Operations\n\nIn the following table there'is a recap of all the supported operations and their safety.\nAn operation is considered safe if it can generate a rounding error and return it to the caller\nor it cannot generate a rounding error in any case.\n\nFor example, the `Split` operation provides different overload methods, both safe and unsafe. \n\nOperation | Safe | Unsafe | Notes\n--- |----|-----| ---\n`MonetaContext.Create` | Yes | Yes | Allocate a new `Money`.\n`Split` | Yes | Yes | Split the value of a `Money` into a list of `Money` instances. There are two overloads: one that takes the number of parts and one that takes a list of weights. Split methods, instead of the rounding error, will return the more significative unallocated part as `Money`.\n`RoundOff` | Yes | Yes | Round off the value of a `Money` to the monetary unit chosen.\n`Apply` | Yes | Yes | Apply a function to the `Money`\n`Add` | Yes | Yes | Adds a numeric value or a compatible `Money`\n`Subtract` | Yes | Yes | Subtracts a numeric value or a compatible `Money`\n`Multiply` | Yes | Yes | Multiplies for a numeric value\n`Divide`| Yes | Yes | Divides by a numeric value or another `Money`\n`Negate` | Yes | No  | Negates the `Money` amount\n+, -, *, / with floating point numbers| No | Yes | The arithmetic operators. Binary operators between a `Money` value and a floating point number are unsafe operations.\n+, - with `Money` values with the same `Currency` or integral numbers or between any kind of `UndefinedCurrency` | Yes | No  | Binary operators between `Money` values with the same `Currency` or integral numbers are safe operations\n+, - with `Money` values with `UndefinedCurrency` with different `DecimalPlaces` | No | Yes | Binary operators between `Money` values with `UndefinedCurrency` a number of `DecimalPlaces` greather than the value with a defined currency are unsafe.\n\n\n### Currency System\n\n#### Undefined Currency\n\nThe Undefined Currency is a special currency identified by the ISO 4217 code `XXX`. It represents a `Money` without a currency. You can create multiple variants of `UndefinedCurrency` with different `DecimalPlaces`.\n\n#### Currency Compatibility and Binary Operations\n\nTwo `Currencies` are considered compatible if they share the same `Code`.\n\nBinary operations between two `Money` instances are allowed only if their `Currencies` are compatible.\n\n### Currency Providers\n\nMoneta offers numerous extension points for customizing the library's behavior. One of the most important is the `ICurrencyProvider`, which enables you to seamlessly integrate your currency system. For example, you can implement an `ICurrencyProvider` to resolve currencies from a database or a web service.\n\nMoneta provides the following providers:\n* `NullCurrencyProvider`, which doesn't resolve any currency.\n* `IsoCurrencyProvider`, which resolves currencies from the ISO 4217 standard. This provider can be customized to resolve a subset of the standard.\n* `ChainOfCurrencyProvider`, which resolves currencies from a list of other providers, suitable for caching and fallback scenarios.\n\n### Exchange Rate Conversion\n\nTBD (To Be Determined)\n\n### Exchange Rate Providers\n\nTBD (To Be Determined)\n\n## Samples\n\nThese samples are available in the [Moneta.Samples](./samples/MonetaHelloWorld) project.\n\n### Sample 1: no rounding errors occurred\n\n```csharp\nusing (var context = new MonetaContex(options))\n{\n\tvar money = context.CreateMoney(1.00M);\n\n\tmoney += 11;\n\tmoney /= 2;\n\t\n\tConsole.WriteLine($\"The final amount is {money}\");\n} // OK!\n```\n### Sample 2: handled rounding errors\n\n```csharp\nusing (var context = new MonetaContex(options)))\n{\n\tvar money = context.CreateMoney(1.00M);\n\n\tmoney += 11;\n\tmoney /= 2;\n\tmoney += 1.2321; // Unhandled Rounding error\n\t\n\tif (context.HasRoundingErrors)\n\t{\n\t\tConsole.WriteLine(\" \u003e Rounding errors detected\");\n\t\tforeach (var error in context.RoundingErrors)\n\t\t{\n\t\t\t// TODO: Handle rounding errors\n\t\t\tConsole.WriteLine($\"   Error: {error}\");\n\t\t}\n\t\tcontext.ClearRoundingErrors();\n\t}\n\t\n\tConsole.WriteLine($\"The final amount is {money}\");\n} // OK!\n```\n### Sample 3: unhanded rounding errors\n\n```csharp\nusing (var context = new MonetaContex(options))\n{\n\tvar money = context.CreateMoney(1.00M);\n\n\tmoney += 11;\n\tmoney /= 2;\n\tmoney += 1.2321; // Unhandled Rounding error\n\t\n\tConsole.WriteLine($\"The final amount is {money}\");\n} // KO! Exception thrown\n```\n\n###  Sample 4: weighted split with unallocated money and rounding error\n\n```csharp\nusing (var context = new MonetaContex(options))\n{\n    var money = context.CreateMoney(11.11);\n    var weights = Enumerable.Repeat(0.333333, 3);\n\n\tvar split = money.Split(weights, out var unallocated);\n\n\tConsole.WriteLine($\"The original amount is {money}\");\n\tConsole.WriteLine($\"The allocated amounts are: {string.Join(\", \", split)}\");\n\tConsole.WriteLine($\"The unallocated amount is {unallocated}\");\n} // OK!\n```\n\nwill produce the following output:\n\n```\nThe original amount is EUR 11.11\nThe allocated amounts are: EUR 3.70, EUR 3.70, EUR 3.70\nThe unallocated amount is EUR 0.01\n\n```\n\n### Sample 5: Round-Off Cash to 0.05 EUR (Cash rounding)\n    \n```csharp\nusing (var context = new MonetaContex(options))\n{\nConsole.WriteLine(\"\\nSample 5: Rounding the final amount to the nearest 0.05 EUR (Cash rounding)\");\nvar amounts = Enumerable.Repeat(context.CreateMoney(3.37), 17);\n\n\tvar total = amounts.Aggregate((x, y) =\u003e x + y);  // sum up all the amounts\n\tvar cashUnit = context.CreateMoney(0.05); // define the cash unit\n\t// round off the total to the highest multiple of the cash unit that is less than or equal to the total\n\t// a kindness to our customers that always save some pennies :)\n\tvar cashTotal = total.RoundOff(cashUnit, MidpointRounding.ToZero, out var unallocated);\n\n\tConsole.WriteLine($\"The original total amount is {total}\");\n\tConsole.WriteLine($\"The cash total amount is {cashTotal}\");\n\tConsole.WriteLine($\"The discounted amount is {unallocated}\");\n} // OK!\n```\n\nwill produce the following output:\n\n```\nSample 5: Rounding the final amount to the nearest 0.05 EUR (Cash rounding)\nThe original total amount is 57.29\nThe cash total amount is 57.25\nThe discounted amount is 0.04\n```\n\n### Sample 6: Calculating the P/E Ratio\n\n```csharp\nusing (var context = new MonetaContex(options))\n{\n    Console.WriteLine(\"\\nSample 6: Calculating the P/E Ratio\");\n    var price = context.CreateMoney(100);\n    var earnings = context.CreateMoney(10);\n\n\tvar peRatio = price / earnings;\n\n\tConsole.WriteLine($\"The P/E Ratio is {peRatio}\");\n} // OK!\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbogoware%2Fmoneta","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbogoware%2Fmoneta","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbogoware%2Fmoneta/lists"}