{"id":15109009,"url":"https://github.com/humanizr/humanizer","last_synced_at":"2026-03-05T09:10:16.491Z","repository":{"id":3181511,"uuid":"4213474","full_name":"Humanizr/Humanizer","owner":"Humanizr","description":"Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities","archived":false,"fork":false,"pushed_at":"2025-05-05T17:51:37.000Z","size":8619,"stargazers_count":8888,"open_issues_count":187,"forks_count":977,"subscribers_count":194,"default_branch":"main","last_synced_at":"2025-05-05T20:53:29.428Z","etag":null,"topics":["hacktoberfest","localization"],"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/Humanizr.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"license.txt","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2012-05-03T12:01:45.000Z","updated_at":"2025-05-05T17:51:41.000Z","dependencies_parsed_at":"2023-07-11T11:02:55.438Z","dependency_job_id":"890633a7-33ab-43c1-8daa-e30c52dc7cd8","html_url":"https://github.com/Humanizr/Humanizer","commit_stats":{"total_commits":1770,"total_committers":252,"mean_commits":7.023809523809524,"dds":0.7853107344632768,"last_synced_commit":"1bdd7a1bad03fd001d04fc0e252d279b4e785818"},"previous_names":["mehdik/humanizer"],"tags_count":70,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Humanizr%2FHumanizer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Humanizr%2FHumanizer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Humanizr%2FHumanizer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Humanizr%2FHumanizer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Humanizr","download_url":"https://codeload.github.com/Humanizr/Humanizer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253926472,"owners_count":21985519,"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","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":["hacktoberfest","localization"],"created_at":"2024-09-25T23:00:30.092Z","updated_at":"2026-03-05T09:10:16.437Z","avatar_url":"https://github.com/Humanizr.png","language":"C#","readme":"﻿# \u003cimg width=\"30px\" src=\"logo.png\" /\u003e Humanizer\r\n\r\n[![Build Status](https://dev.azure.com/dotnet/Humanizer/_apis/build/status/Humanizer-CI?branchName=main)](https://dev.azure.com/dotnet/Humanizer/_build?definitionId=14)\r\n[![NuGet version](https://img.shields.io/nuget/v/Humanizer.svg?logo=nuget\u0026cacheSeconds=300)](https://www.nuget.org/packages/Humanizer)\r\n[![NuGet version prerelease](https://img.shields.io/nuget/vpre/Humanizer.svg?logo=nuget\u0026cacheSeconds=300)](https://img.shields.io/nuget/vpre/Humanizer.Core)\r\n[![NuGet downloads](https://img.shields.io/nuget/dt/Humanizer.Core.svg?logo=nuget\u0026cacheSeconds=300)](https://www.nuget.org/packages/Humanizer.Core)\r\n\r\nHumanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities.\r\n\r\n\u003e **📚 Full Documentation**: See the [documentation folder](docs/index.md) for comprehensive guides, examples, and API reference.\r\n\r\n\r\n## Install\r\n\r\nYou can install Humanizer as [a NuGet package](https://www.nuget.org/packages/Humanizer):\r\n\r\n**English only**: `Humanizer.Core`\r\n\r\nAll languages: `Humanizer`\r\n\r\n\r\n### Supported frameworks\r\n\r\n**Supported frameworks:** net10.0, net8.0, net48\r\n\r\n**netstandard2.0:** The NuGet package also targets netstandard2.0 as a special case to support specific scenarios such as Roslyn Analyzers and MSBuild tasks that require netstandard2.0.\r\n\r\n**Unsupported versions:** While other .NET Framework versions (net4.6.1 through net4.7.2) can technically consume netstandard2.0 libraries, they are **not officially supported** by Humanizer and may not work correctly. Please use one of the explicitly supported frameworks listed above for the best experience.\r\n\r\nHumanizer symbols are source-indexed with [SourceLink](https://github.com/dotnet/sourcelink) and included in the package, which means you can step through Humanizer's source code while debugging your own application.\r\n\r\n\u003e [!IMPORTANT]\r\n\u003e **Humanizer 3.0 restore requirements**: The `Humanizer` metapackage now requires the NuGet locale parsing fix shipped in [.NET SDK 9.0.200 and corresponding Visual Studio/MSBuild updates](https://github.com/NuGet/NuGet.Client/pull/6124#issuecomment-3391090183). Restore operations must run on .NET SDK 9.0.200 or newer, or on Visual Studio 2022/MSBuild versions that include that patch. Older SDKs/MSBuild builds will fail to restore the metapackage because they do not recognize three-letter locale identifiers. As a workaround, reference `Humanizer.Core` directly and add the desired `Humanizer.Core.\u003clocale\u003e` satellite packages individually when targeting older tooling.\r\n\r\n\u003e Upgrading from `2.14.1` to `3.0.8`? See [docs/migration-v3.md](docs/migration-v3.md) for the complete breaking-change checklist, patch-line fix notes, and known regression status.\r\n\r\n### Specify Languages (Optional)\r\n\r\nYou choose which packages based on what NuGet package(s) you install. By default, the main `Humanizer` package installs all supported languages. If you're not sure, then just use the main `Humanizer` package.\r\n\r\nHere are the options:\r\n\r\n| Option | Package Name | Install Command | Included Languages |\r\n|--------|--------------|-----------------|-------------------|\r\n| **All languages** | `Humanizer` | `dotnet add package Humanizer` | All supported languages (pulls in `Humanizer.Core` and all language packages) |\r\n| **English only** | `Humanizer.Core` | `dotnet add package Humanizer.Core` | English only |\r\n| **Specific languages** | `Humanizer.Core.\u003clocale\u003e` | `dotnet add package Humanizer.Core.fr` (French example) | Install as many language-specific packages as needed |\r\n\r\nFor example, for French use `Humanizer.Core.fr`, for Spanish use `Humanizer.Core.es`, etc. You can include multiple languages by installing however many language packages you want.\r\n\r\nThe detailed explanation for how this works is in the comments [here](https://github.com/Humanizr/Humanizer/issues/59#issuecomment-152546079).\r\n\r\n\r\n## Features\r\n\r\n\r\n### Humanize String\r\n\r\nString humanization is a core feature that transforms computerized strings into readable, human-friendly text. This is particularly valuable when you need to display programming identifiers (class names, method names, properties) to end users in a readable format.\r\n\r\nThe foundation of this feature was originally developed for the [BDDfy framework](https://github.com/TestStack/TestStack.BDDfy) to turn test method names into readable test descriptions. `Humanize` intelligently handles PascalCase, camelCase, underscored_strings, and dash-separated-strings.\r\n\r\n```csharp\r\n\"PascalCaseInputStringIsTurnedIntoSentence\".Humanize() \r\n    =\u003e \"Pascal case input string is turned into sentence\"\r\n\r\n\"Underscored_input_string_is_turned_into_sentence\".Humanize() \r\n    =\u003e \"Underscored input string is turned into sentence\"\r\n\r\n\"dash-separated-string\".Humanize() \r\n    =\u003e \"Dash separated string\"\r\n```\r\n\r\n**Acronym Handling**: Strings containing only uppercase letters are treated as acronyms and left unchanged. To humanize any string, use the `Transform` method:\r\n\r\n```csharp\r\n\"HTML\".Humanize() =\u003e \"HTML\"  // Acronym preserved\r\n\"HUMANIZER\".Humanize() =\u003e \"HUMANIZER\"  // All caps preserved\r\n\r\n// Force humanization with Transform\r\n\"HUMANIZER\".Transform(To.LowerCase, To.TitleCase) =\u003e \"Humanizer\"\r\n```\r\n\r\n**Letter Casing**: Control the output casing directly:\r\n\r\n```csharp\r\n\"CanReturnTitleCase\".Humanize(LetterCasing.Title) =\u003e \"Can Return Title Case\"\r\n\"CanReturnLowerCase\".Humanize(LetterCasing.LowerCase) =\u003e \"can return lower case\"\r\n\"CanHumanizeIntoUpperCase\".Humanize(LetterCasing.AllCaps) =\u003e \"CAN HUMANIZE INTO UPPER CASE\"\r\n\"some string\".Humanize(LetterCasing.Sentence) =\u003e \"Some string\"\r\n```\r\n\r\n#### Version 3.0 Behavioral Change\r\n\r\nIn version 3.0, `Humanize` and `Titleize` now preserve input strings that contain no recognized letters (e.g., special characters, unrecognized Unicode scripts) instead of returning an empty string:\r\n\r\n```csharp\r\n// Before v3.0: returned \"\"\r\n// v3.0 and later: returns \"@@\"\r\n\"@@\".Humanize() =\u003e \"@@\"\r\n\r\n// Cyrillic and other Unicode scripts are also preserved\r\n\"Майк\".Titleize() =\u003e \"Майк\"\r\n```\r\n\r\n\r\n### Dehumanize String\r\n\r\nReverse the humanization process by converting human-friendly strings back to PascalCase:\r\n\r\n```csharp\r\n\"Pascal case input string is turned into sentence\".Dehumanize() \r\n    =\u003e \"PascalCaseInputStringIsTurnedIntoSentence\"\r\n\r\n\"some string\".Dehumanize() =\u003e \"SomeString\"\r\n\"Some String\".Dehumanize() =\u003e \"SomeString\"\r\n```\r\n\r\n\r\n### Transform String\r\n\r\nThe `Transform` method provides a flexible, extensible way to apply string transformations. Unlike the legacy `LetterCasing` enum which limits you to built-in options, `IStringTransformer` is an interface you can implement in your codebase for domain-specific transformations.\r\n\r\n```csharp\r\n\"Sentence casing\".Transform(To.LowerCase) =\u003e \"sentence casing\"\r\n\"Sentence casing\".Transform(To.SentenceCase) =\u003e \"Sentence casing\"\r\n\"Sentence casing\".Transform(To.TitleCase) =\u003e \"Sentence Casing\"\r\n\"Sentence casing\".Transform(To.UpperCase) =\u003e \"SENTENCE CASING\"\r\n```\r\n\r\n\r\n\r\n\r\n### Truncate String\r\n\r\nIntelligently truncate strings with multiple strategies. The default uses the `…` character (one character) instead of `\"...\"` (three characters) to maximize visible text before truncation. You can also implement custom `ITruncator` strategies for specialized truncation logic.\r\n\r\n```csharp\r\n\"Long text to truncate\".Truncate(10) =\u003e \"Long text…\"\r\n\"Long text to truncate\".Truncate(10, \"---\") =\u003e \"Long te---\"\r\n\r\n// Fixed length (default)\r\n\"Long text to truncate\".Truncate(10, Truncator.FixedLength) =\u003e \"Long text…\"\r\n\r\n// Fixed number of words\r\n\"Long text to truncate\".Truncate(2, Truncator.FixedNumberOfWords) =\u003e \"Long text…\"\r\n\r\n// Truncate from the left\r\n\"Long text to truncate\".Truncate(10, Truncator.FixedLength, TruncateFrom.Left) \r\n    =\u003e \"… truncate\"\r\n```\r\n\r\n\r\n\r\n\r\n### Humanize Enums\r\n\r\nEnum humanization eliminates the need to manually add spaces between words in enum member names. While you could use `DescriptionAttribute` on every enum member, Humanizer automatically handles the common case of simply needing to add spaces. When `DescriptionAttribute` (or any attribute with a `Description` property) is present, it takes precedence, making it easy to provide custom text only when needed.\r\n\r\n```csharp\r\npublic enum UserType\r\n{\r\n    [Description(\"Custom description\")]\r\n    MemberWithDescriptionAttribute,\r\n    MemberWithoutDescriptionAttribute,\r\n    ALLCAPITALS\r\n}\r\n\r\n// DescriptionAttribute value is used\r\nUserType.MemberWithDescriptionAttribute.Humanize() =\u003e \"Custom description\"\r\n\r\n// Automatic humanization when no attribute\r\nUserType.MemberWithoutDescriptionAttribute.Humanize() \r\n    =\u003e \"Member without description attribute\"\r\n\r\n// Apply casing transformations\r\nUserType.MemberWithoutDescriptionAttribute.Humanize().Transform(To.TitleCase) \r\n    =\u003e \"Member Without Description Attribute\"\r\n```\r\n\r\nHumanizer works with any attribute containing a `Description` property and supports localized `DisplayAttribute` for multi-language scenarios. This helps you avoid littering enums with unnecessary attributes while still providing customization when needed.\r\n\r\n\r\n### Dehumanize Enums\r\n\r\nConvert humanized strings back to their original enum values:\r\n\r\n```csharp\r\n\"Member without description attribute\".DehumanizeTo\u003cUserType\u003e() \r\n    =\u003e UserType.MemberWithoutDescriptionAttribute\r\n\r\n// Non-generic version for runtime types\r\n\"Member without description attribute\".DehumanizeTo(typeof(UserType)) \r\n    =\u003e UserType.MemberWithoutDescriptionAttribute\r\n\r\n// Handle missing matches gracefully\r\n\"Invalid\".DehumanizeTo\u003cUserType\u003e(OnNoMatch.ReturnsNull) =\u003e null\r\n```\r\n\r\nThe method honors `DescriptionAttribute` and is case-insensitive.\r\n\r\n\r\n### Humanize DateTime\r\n\r\nGet relative time descriptions for `DateTime` and `DateTimeOffset` values:\r\n\r\n```csharp\r\nDateTime.UtcNow.AddHours(-2).Humanize() =\u003e \"2 hours ago\"\r\nDateTime.UtcNow.AddHours(-30).Humanize() =\u003e \"yesterday\"\r\n\r\nDateTime.UtcNow.AddHours(2).Humanize() =\u003e \"2 hours from now\"\r\nDateTime.UtcNow.AddHours(30).Humanize() =\u003e \"tomorrow\"\r\n\r\nDateTimeOffset.UtcNow.AddHours(1).Humanize() =\u003e \"an hour from now\"\r\n```\r\n\r\nSupports both UTC and local times, with optional culture specification and custom comparison dates.\r\n\r\n```csharp\r\npublic static string Humanize(this DateTime input, bool utcDate = true, DateTime? dateToCompareAgainst = null, CultureInfo culture = null)\r\npublic static string Humanize(this DateTimeOffset input, DateTimeOffset? dateToCompareAgainst = null, CultureInfo culture = null)\r\n```\r\n\r\nMany localizations are available for this method. Here are a few examples:\r\n\r\n```csharp\r\n// In ar culture\r\nDateTime.UtcNow.AddDays(-1).Humanize() =\u003e \"أمس\"\r\nDateTime.UtcNow.AddDays(-2).Humanize() =\u003e \"منذ يومين\"\r\nDateTime.UtcNow.AddDays(-3).Humanize() =\u003e \"منذ 3 أيام\"\r\nDateTime.UtcNow.AddDays(-11).Humanize() =\u003e \"منذ 11 يوم\"\r\n\r\n// In ru-RU culture\r\nDateTime.UtcNow.AddMinutes(-1).Humanize() =\u003e \"минуту назад\"\r\nDateTime.UtcNow.AddMinutes(-2).Humanize() =\u003e \"2 минуты назад\"\r\nDateTime.UtcNow.AddMinutes(-10).Humanize() =\u003e \"10 минут назад\"\r\nDateTime.UtcNow.AddMinutes(-21).Humanize() =\u003e \"21 минуту назад\"\r\nDateTime.UtcNow.AddMinutes(-22).Humanize() =\u003e \"22 минуты назад\"\r\nDateTime.UtcNow.AddMinutes(-40).Humanize() =\u003e \"40 минут назад\"\r\n```\r\n\r\nThere are two strategies for `DateTime.Humanize`: the default one as seen above and a precision based one.\r\nTo use the precision based strategy you need to configure it:\r\n\r\n```csharp\r\nConfigurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision: .75);\r\nConfigurator.DateTimeOffsetHumanizeStrategy = new PrecisionDateTimeOffsetHumanizeStrategy(precision: .75); // configure when humanizing DateTimeOffset\r\n```\r\n\r\nThe default precision is set to .75, but you can pass your desired precision too. With precision set to 0.75:\r\n\r\n```csharp\r\n44 seconds =\u003e 44 seconds ago/from now\r\n45 seconds =\u003e one minute ago/from now\r\n104 seconds =\u003e one minute ago/from now\r\n105 seconds =\u003e two minutes ago/from now\r\n\r\n25 days =\u003e a month ago/from now\r\n```\r\n\r\n**No dehumanization for dates as `Humanize` is a lossy transformation and the human friendly date is not reversible**\r\n\r\n\r\n### Humanize TimeSpan\r\n\r\nYou can call `Humanize` on a `TimeSpan` to a get human friendly representation for it:\r\n\r\n```csharp\r\nTimeSpan.FromMilliseconds(1).Humanize() =\u003e \"1 millisecond\"\r\nTimeSpan.FromMilliseconds(2).Humanize() =\u003e \"2 milliseconds\"\r\nTimeSpan.FromDays(1).Humanize() =\u003e \"1 day\"\r\nTimeSpan.FromDays(16).Humanize() =\u003e \"2 weeks\"\r\n```\r\n\r\nThere is an optional `precision` parameter for `TimeSpan.Humanize` which allows you to specify the precision of the returned value.\r\nThe default value of `precision` is 1 which means only the largest time unit is returned like you saw in `TimeSpan.FromDays(16).Humanize()`.\r\nHere are a few examples of specifying precision:\r\n\r\n```csharp\r\nTimeSpan.FromDays(1).Humanize(precision:2) =\u003e \"1 day\" // no difference when there is only one unit in the provided TimeSpan\r\nTimeSpan.FromDays(16).Humanize(2) =\u003e \"2 weeks, 2 days\"\r\n\r\n// the same TimeSpan value with different precision returns different results\r\nTimeSpan.FromMilliseconds(1299630020).Humanize() =\u003e \"2 weeks\"\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(3) =\u003e \"2 weeks, 1 day, 1 hour\"\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(4) =\u003e \"2 weeks, 1 day, 1 hour, 30 seconds\"\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(5) =\u003e \"2 weeks, 1 day, 1 hour, 30 seconds, 20 milliseconds\"\r\n```\r\n\r\nBy default, when using `precision` parameter empty time units are not counted towards the precision of the returned value.\r\nIf this behavior isn't desired for you, you can use the overloaded `TimeSpan.Humanize` method with `countEmptyUnits` parameter. Leading empty time units never count.\r\nHere is an example showing the difference of counting empty units:\r\n\r\n```csharp\r\nTimeSpan.FromMilliseconds(3603001).Humanize(3) =\u003e \"1 hour, 3 seconds, 1 millisecond\"\r\nTimeSpan.FromMilliseconds(3603001).Humanize(3, countEmptyUnits:true) =\u003e \"1 hour, 3 seconds\"\r\n```\r\n\r\nMany localizations are available for this method:\r\n\r\n```csharp\r\n// in de-DE culture\r\nTimeSpan.FromDays(1).Humanize() =\u003e \"Ein Tag\"\r\nTimeSpan.FromDays(2).Humanize() =\u003e \"2 Tage\"\r\n\r\n// in sk-SK culture\r\nTimeSpan.FromMilliseconds(1).Humanize() =\u003e \"1 milisekunda\"\r\nTimeSpan.FromMilliseconds(2).Humanize() =\u003e \"2 milisekundy\"\r\nTimeSpan.FromMilliseconds(5).Humanize() =\u003e \"5 milisekúnd\"\r\n```\r\n\r\nCulture to use can be specified explicitly. If it is not, current thread's current UI culture is used. Example:\r\n\r\n```csharp\r\nTimeSpan.FromDays(1).Humanize(culture: \"ru-RU\") =\u003e \"один день\"\r\n```\r\n\r\nIn addition, a minimum unit of time may be specified to avoid rolling down to a smaller unit. For example:\r\n  ```csharp\r\n  TimeSpan.FromMilliseconds(122500).Humanize(minUnit: TimeUnit.Second) =\u003e \"2 minutes, 2 seconds\"    // instead of 2 minutes, 2 seconds, 500 milliseconds\r\n  TimeSpan.FromHours(25).Humanize(minUnit: TimeUnit.Day) =\u003e \"1 Day\"   //instead of 1 Day, 1 Hour\r\n  ```\r\n\r\nIn addition, a maximum unit of time may be specified to avoid rolling up to the next largest unit. For example:\r\n```csharp\r\nTimeSpan.FromDays(7).Humanize(maxUnit: TimeUnit.Day) =\u003e \"7 days\"    // instead of 1 week\r\nTimeSpan.FromMilliseconds(2000).Humanize(maxUnit: TimeUnit.Millisecond) =\u003e \"2000 milliseconds\"    // instead of 2 seconds\r\n```\r\nThe default maxUnit is `TimeUnit.Week` because it gives exact results. You can increase this value to `TimeUnit.Month` or `TimeUnit.Year` which will give you an approximation based on 365.2425 days a year and 30.436875 days a month. Therefore, the months are alternating between 30 and 31 days in length and every fourth year is 366 days long.\r\n```csharp\r\nTimeSpan.FromDays(486).Humanize(maxUnit: TimeUnit.Year, precision: 7) =\u003e \"1 year, 3 months, 29 days\" // One day further is 1 year, 4 month\r\nTimeSpan.FromDays(517).Humanize(maxUnit: TimeUnit.Year, precision: 7) =\u003e \"1 year, 4 months, 30 days\" // This month has 30 days and one day further is 1 year, 5 months\r\n```\r\n\r\nWhen there are multiple time units, they are combined using the `\", \"` string:\r\n\r\n```csharp\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(3) =\u003e \"2 weeks, 1 day, 1 hour\"\r\n```\r\n\r\nWhen `TimeSpan` is zero, the default behavior will return \"0\" plus whatever the minimum time unit is. However, if you assign `true` to `toWords` when calling `Humanize`, then the method returns \"no time\". For example:\r\n```csharp\r\nTimeSpan.Zero.Humanize(1) =\u003e \"0 milliseconds\"\r\nTimeSpan.Zero.Humanize(1, toWords: true) =\u003e \"no time\"\r\nTimeSpan.Zero.Humanize(1, minUnit: Humanizer.Localisation.TimeUnit.Second) =\u003e \"0 seconds\"\r\n```\r\n\r\nUsing the `collectionSeparator` parameter, you can specify your own separator string:\r\n\r\n```csharp\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(3, collectionSeparator: \" - \") =\u003e \"2 weeks - 1 day - 1 hour\"\r\n````\r\n\r\nIt is also possible to use the current culture's collection formatter to combine the time units. To do so, specify `null` as the `collectionSeparator` parameter:\r\n\r\n```csharp\r\n// in en-US culture\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(3, collectionSeparator: null) =\u003e \"2 weeks, 1 day, and 1 hour\"\r\n\r\n// in de-DE culture\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(3, collectionSeparator: null) =\u003e \"2 Wochen, Ein Tag und Eine Stunde\"\r\n```\r\n\r\nIf words are preferred to numbers, a `toWords: true` parameter can be set to convert the numbers in a humanized TimeSpan to words:\r\n```csharp\r\nTimeSpan.FromMilliseconds(1299630020).Humanize(3, toWords: true) =\u003e \"two weeks, one day, one hour\"\r\n```\r\n\r\nBy calling `ToAge`, a `TimeSpan` can also be expressed as an age.\r\nFor cultures that do not define an age expression, the result will be the same as calling `Humanize` _(but with a default `maxUnit` equal to `TimeUnit.Year`)_. \r\n\r\n```csharp\r\n// in en-US culture\r\nTimeSpan.FromDays(750).ToAge() =\u003e \"2 years old\"\r\n\r\n// in fr culture\r\nTimeSpan.FromDays(750).ToAge() =\u003e \"2 ans\"\r\n```\r\n\r\n\r\n### Humanize Collections\r\n\r\nYou can call `Humanize` on any `IEnumerable` to get a nicely formatted string representing the objects in the collection. By default `ToString()` will be called on each item to get its representation but a formatting function may be passed to `Humanize` instead. Additionally, a default separator is provided (\"and\" in English), but a different separator may be passed into `Humanize`.\r\n\r\nFor instance:\r\n\r\n```csharp\r\nclass SomeClass\r\n{\r\n    public string SomeString;\r\n    public int SomeInt;\r\n    public override string ToString()\r\n    {\r\n        return \"Specific String\";\r\n    }\r\n}\r\n\r\nstring FormatSomeClass(SomeClass sc)\r\n{\r\n    return string.Format(\"SomeObject #{0} - {1}\", sc.SomeInt, sc.SomeString);\r\n}\r\n\r\nvar collection = new List\u003cSomeClass\u003e\r\n{\r\n    new SomeClass { SomeInt = 1, SomeString = \"One\" },\r\n    new SomeClass { SomeInt = 2, SomeString = \"Two\" },\r\n    new SomeClass { SomeInt = 3, SomeString = \"Three\" }\r\n};\r\n\r\ncollection.Humanize()                                    // \"Specific String, Specific String, and Specific String\"\r\ncollection.Humanize(\"or\")                                // \"Specific String, Specific String, or Specific String\"\r\ncollection.Humanize(FormatSomeClass)                     // \"SomeObject #1 - One, SomeObject #2 - Two, and SomeObject #3 - Three\"\r\ncollection.Humanize(sc =\u003e sc.SomeInt.Ordinalize(), \"or\") // \"1st, 2nd, or 3rd\"\r\n```\r\n\r\nItems are trimmed and blank (NullOrWhitespace) items are skipped. This results in clean comma punctuation. (If there is a custom formatter function, this applies only to the formatter's output.)\r\n\r\nYou can provide your own collection formatter by implementing `ICollectionFormatter` and registering it with `Configurator.CollectionFormatters`.\r\n\r\n\r\n### Inflector methods\r\n\r\nInflector methods handle the complexities of English pluralization and singularization rules, including irregular words (man/men, person/people) and uncountable words (fish, equipment). These methods save you from maintaining your own word lists and rules.\r\n\r\n\r\n#### Pluralize\r\n\r\nIntelligently pluralize words, handling irregular and uncountable forms:\r\n\r\n```csharp\r\n\"Man\".Pluralize() =\u003e \"Men\"\r\n\"string\".Pluralize() =\u003e \"strings\"\r\n\"person\".Pluralize() =\u003e \"people\"\r\n\r\n// Uncertain plurality? Use the optional parameter\r\n\"Men\".Pluralize(inputIsKnownToBeSingular: false) =\u003e \"Men\"\r\n\"string\".Pluralize(inputIsKnownToBeSingular: false) =\u003e \"strings\"\r\n```\r\n\r\n\r\n#### Singularize\r\n\r\nConvert plural words to singular form:\r\n\r\n```csharp\r\n\"Men\".Singularize() =\u003e \"Man\"\r\n\"strings\".Singularize() =\u003e \"string\"\r\n\"people\".Singularize() =\u003e \"person\"\r\n\r\n// Uncertain plurality? Use the optional parameter\r\n\"Man\".Singularize(inputIsKnownToBePlural: false) =\u003e \"Man\"\r\n```\r\n\r\n\r\n## Adding Words\r\n\r\nSometimes, you may need to add a rule from the singularization/pluralization vocabulary (the examples below are already in the `DefaultVocabulary` used by `Inflector`):\r\n\r\n```csharp\r\n// Adds a word to the vocabulary which cannot easily be pluralized/singularized by RegEx.\r\n// Will match both \"salesperson\" and \"person\".\r\nVocabularies.Default.AddIrregular(\"person\", \"people\");\r\n\r\n// To only match \"person\" and not \"salesperson\" you would pass false for the 'matchEnding' parameter.\r\nVocabularies.Default.AddIrregular(\"person\", \"people\", matchEnding: false);\r\n\r\n// Adds an uncountable word to the vocabulary.  Will be ignored when plurality is changed:\r\nVocabularies.Default.AddUncountable(\"fish\");\r\n\r\n// Adds a rule to the vocabulary that does not follow trivial rules for pluralization:\r\nVocabularies.Default.AddPlural(\"bus\", \"buses\");\r\n\r\n// Adds a rule to the vocabulary that does not follow trivial rules for singularization\r\n// (will match both \"vertices\" -\u003e \"vertex\" and \"indices\" -\u003e \"index\"):\r\nVocabularies.Default.AddSingular(\"(vert|ind)ices$\", \"$1ex\");\r\n\r\n```\r\n\r\n\r\n#### ToQuantity\r\n\r\nCombine numbers with properly pluralized/singularized words:\r\n\r\n```csharp\r\n\"case\".ToQuantity(0) =\u003e \"0 cases\"\r\n\"case\".ToQuantity(1) =\u003e \"1 case\"\r\n\"case\".ToQuantity(5) =\u003e \"5 cases\"\r\n\"man\".ToQuantity(2) =\u003e \"2 men\"\r\n\r\n// Display quantity as words or hide it\r\n\"case\".ToQuantity(5, ShowQuantityAs.Words) =\u003e \"five cases\"\r\n\"case\".ToQuantity(5, ShowQuantityAs.None) =\u003e \"cases\"\r\n\r\n// Format with custom number formatting\r\n\"dollar\".ToQuantity(2, \"C0\", new CultureInfo(\"en-US\")) =\u003e \"$2 dollars\"\r\n\"cases\".ToQuantity(12000, \"N0\") =\u003e \"12,000 cases\"\r\n```\r\n\r\n\r\n#### Ordinalize\r\n\r\nConvert numbers to ordinal strings (1st, 2nd, 3rd, etc.):\r\n\r\n```csharp\r\n1.Ordinalize() =\u003e \"1st\"\r\n5.Ordinalize() =\u003e \"5th\"\r\n21.Ordinalize() =\u003e \"21st\"\r\n\"21\".Ordinalize() =\u003e \"21st\"\r\n\r\n// Supports grammatical gender (culture-specific)\r\n1.Ordinalize(GrammaticalGender.Feminine) =\u003e \"1ª\"  // Brazilian Portuguese\r\n1.Ordinalize(GrammaticalGender.Masculine) =\u003e \"1º\"  // Brazilian Portuguese\r\n```\r\n\r\n\r\n#### Titleize, Pascalize, Camelize\r\n\r\nConvert strings to various coding conventions:\r\n\r\n```csharp\r\n// Titleize: Title Case with spaces\r\n\"some_title\".Titleize() =\u003e \"Some Title\"\r\n\r\n// Pascalize: UpperCamelCase without spaces\r\n\"some_title for something\".Pascalize() =\u003e \"SomeTitleForSomething\"\r\n\r\n// Camelize: lowerCamelCase without spaces\r\n\"some_title for something\".Camelize() =\u003e \"someTitleForSomething\"\r\n```\r\n\r\n\r\n#### Underscore, Dasherize, Kebaberize\r\n\r\nTransform to common naming conventions:\r\n\r\n```csharp\r\n// Underscore: snake_case\r\n\"SomeTitle\".Underscore() =\u003e \"some_title\"\r\n\r\n// Dasherize/Hyphenate: Replace underscores with dashes\r\n\"some_title\".Dasherize() =\u003e \"some-title\"\r\n\"some_title\".Hyphenate() =\u003e \"some-title\"\r\n\r\n// Kebaberize: kebab-case (lowercase with hyphens)\r\n\"SomeText\".Kebaberize() =\u003e \"some-text\"\r\n\"some property name\".Kebaberize() =\u003e \"some-property-name\"\r\n```\r\n\r\n\r\n### Fluent Date\r\n\r\nFluent date methods make time-based code dramatically more readable. Instead of verbose `DateTime.Now.AddDays(2).AddHours(3)` calls, you can write `DateTime.Now + 2.Days() + 3.Hours()`. This improves code clarity and reduces errors.\r\n\r\n**TimeSpan methods:**\r\n\r\n```csharp\r\n2.Milliseconds() =\u003e TimeSpan.FromMilliseconds(2)\r\n5.Seconds() =\u003e TimeSpan.FromSeconds(5)\r\n3.Minutes() =\u003e TimeSpan.FromMinutes(3)\r\n4.Hours() =\u003e TimeSpan.FromHours(4)\r\n7.Days() =\u003e TimeSpan.FromDays(7)\r\n2.Weeks() =\u003e TimeSpan.FromDays(14)\r\n```\r\n\r\n**Use fluent syntax instead of verbose `Add` methods:**\r\n\r\n```csharp\r\n// Instead of this:\r\nDateTime.Now.AddDays(2).AddHours(3).AddMinutes(-5)\r\n\r\n// Write this:\r\nDateTime.Now + 2.Days() + 3.Hours() - 5.Minutes()\r\n```\r\n\r\n**DateTime construction:**\r\n\r\n```csharp\r\nIn.TheYear(2010) =\u003e new DateTime(2010, 1, 1)\r\nIn.January =\u003e January 1st of current year\r\nIn.FebruaryOf(2009) =\u003e February 1st, 2009\r\n\r\nOn.January.The4th =\u003e January 4th of current year\r\nOn.February.The(12) =\u003e February 12th of current year\r\n```\r\n\r\n**DateTime manipulation:**\r\n\r\n```csharp\r\nvar date = new DateTime(2011, 2, 10, 5, 25, 45, 125);\r\n\r\ndate.In(2008) =\u003e Changes year to 2008\r\ndate.At(2, 20, 15) =\u003e Changes time to 2:20:15.125\r\ndate.AtNoon() =\u003e Changes time to 12:00:00\r\ndate.AtMidnight() =\u003e Changes time to 00:00:00\r\n```\r\n\r\n\r\n### Number to numbers\r\n\r\nCreate large numbers with readable fluent syntax:\r\n\r\n```csharp\r\n3.Thousands() =\u003e 3000\r\n5.Millions() =\u003e 5000000\r\n1.25.Billions() =\u003e 1250000000\r\n\r\n// Chain for complex values\r\n3.Hundreds().Thousands() =\u003e 300000\r\n```\r\n\r\n\r\n### Number to words\r\n\r\nConvert numbers to their word representation:\r\n\r\n```csharp\r\n1.ToWords() =\u003e \"one\"\r\n10.ToWords() =\u003e \"ten\"\r\n122.ToWords() =\u003e \"one hundred and twenty-two\"\r\n3501.ToWords() =\u003e \"three thousand five hundred and one\"\r\n\r\n// Grammatical gender support (culture-specific)\r\n1.ToWords(GrammaticalGender.Masculine, new CultureInfo(\"ru\")) =\u003e \"один\"\r\n1.ToWords(GrammaticalGender.Feminine, new CultureInfo(\"ru\")) =\u003e \"одна\"\r\n\r\n// Control \"and\" usage\r\n3501.ToWords(addAnd: false) =\u003e \"three thousand five hundred one\"\r\n```\r\n\r\n\r\n### Number to ordinal words\r\n\r\nConvert numbers to ordinal word form (first, second, third, etc.):\r\n\r\n```csharp\r\n0.ToOrdinalWords() =\u003e \"zeroth\"\r\n1.ToOrdinalWords() =\u003e \"first\"\r\n2.ToOrdinalWords() =\u003e \"second\"\r\n10.ToOrdinalWords() =\u003e \"tenth\"\r\n21.ToOrdinalWords() =\u003e \"twenty first\"\r\n121.ToOrdinalWords() =\u003e \"hundred and twenty first\"\r\n\r\n// Grammatical gender support (culture-specific)\r\n1.ToOrdinalWords(GrammaticalGender.Masculine, new CultureInfo(\"pt-BR\")) =\u003e \"primeiro\"\r\n1.ToOrdinalWords(GrammaticalGender.Feminine, new CultureInfo(\"pt-BR\")) =\u003e \"primeira\"\r\n```\r\n\r\n### Words to Number Conversion\r\n\r\nConvert English words to numbers (currently English-only):\r\n\r\n```csharp\r\n\"twenty\".ToNumber(new CultureInfo(\"en\")) =\u003e 20\r\n\"one hundred and five\".ToNumber(new CultureInfo(\"en\")) =\u003e 105\r\n\"three thousand two hundred\".ToNumber(new CultureInfo(\"en\")) =\u003e 3200\r\n\r\n// Try pattern for safe conversion\r\nif (\"forty-two\".TryToNumber(out var number, new CultureInfo(\"en\")))\r\n    Console.WriteLine(number); // Outputs: 42\r\n\r\n// Get unrecognized words on failure\r\nif (!\"tenn\".TryToNumber(out var invalid, new CultureInfo(\"en\"), out var badWord))\r\n    Console.WriteLine($\"Unrecognized word: {badWord}\"); // Outputs: Unrecognized word: tenn\r\n```\r\n\r\n\u003e **Note:** Only English (`en`) is currently supported. Other languages throw `NotSupportedException`.\r\n\r\n### DateTime to ordinal words\r\n\r\nConvert dates to ordinal word format:\r\n\r\n```csharp\r\n// English UK\r\nnew DateTime(2015, 1, 1).ToOrdinalWords() =\u003e \"1st January 2015\"\r\nnew DateTime(2015, 3, 22).ToOrdinalWords() =\u003e \"22nd March 2015\"\r\n\r\n// English US\r\nnew DateTime(2015, 1, 1).ToOrdinalWords() =\u003e \"January 1st, 2015\"\r\nnew DateTime(2015, 2, 12).ToOrdinalWords() =\u003e \"February 12th, 2015\"\r\n\r\n// Grammatical case support (culture-specific)\r\nnew DateTime(2015, 1, 1).ToOrdinalWords(GrammaticalCase.Genitive)\r\n```\r\n\r\n\r\n### TimeOnly to Clock Notation\r\n\r\nConvert `TimeOnly` to readable clock notation (.NET 6+):\r\n\r\n```csharp\r\n// English US\r\nnew TimeOnly(3, 0).ToClockNotation() =\u003e \"three o'clock\"\r\nnew TimeOnly(12, 0).ToClockNotation() =\u003e \"noon\"\r\nnew TimeOnly(14, 30).ToClockNotation() =\u003e \"half past two\"\r\n\r\n// Brazilian Portuguese\r\nnew TimeOnly(3, 0).ToClockNotation() =\u003e \"três em ponto\"\r\nnew TimeOnly(14, 30).ToClockNotation() =\u003e \"duas e meia\"\r\n\r\n// Round to nearest 5 minutes\r\nnew TimeOnly(15, 7).ToClockNotation(ClockNotationRounding.NearestFiveMinutes) \r\n    =\u003e \"five past three\"\r\n```\r\n\r\n\r\n### Roman numerals\r\n\r\nConvert between integers and Roman numerals:\r\n\r\n```csharp\r\n// To Roman (supports 1-3999)\r\n1.ToRoman() =\u003e \"I\"\r\n4.ToRoman() =\u003e \"IV\"\r\n10.ToRoman() =\u003e \"X\"\r\n1990.ToRoman() =\u003e \"MCMXC\"\r\n\r\n// From Roman\r\n\"XIV\".FromRoman() =\u003e 14\r\n\"MCMXC\".FromRoman() =\u003e 1990\r\n```\r\n\r\n\r\n### Metric numerals\r\n\r\nConvert between numbers and metric notation:\r\n\r\n```csharp\r\n// To Metric\r\n1d.ToMetric() =\u003e \"1\"\r\n1230d.ToMetric() =\u003e \"1.23k\"\r\n0.1d.ToMetric() =\u003e \"100m\"\r\n456789.ToMetric(decimals: 2) =\u003e \"456.79k\"\r\n\r\n// From Metric\r\n\"1.23k\".FromMetric() =\u003e 1230\r\n\"100m\".FromMetric() =\u003e 0.1\r\n```\r\n\r\n\r\n### ByteSize\r\n\r\nHumanizer includes a port of the brilliant [ByteSize](https://github.com/omar/ByteSize) library.\r\nQuite a few changes and additions are made on `ByteSize` to make the interaction with `ByteSize` easier and more consistent with the Humanizer API.\r\nHere are a few examples of how you can convert from numbers to byte sizes and between size magnitudes:\r\n\r\n```c#\r\nvar fileSize = (10).Kilobytes();\r\n\r\nfileSize.Bits      =\u003e 81920\r\nfileSize.Bytes     =\u003e 10240\r\nfileSize.Kilobytes =\u003e 10\r\nfileSize.Megabytes =\u003e 0.009765625\r\nfileSize.Gigabytes =\u003e 9.53674316e-6\r\nfileSize.Terabytes =\u003e 9.31322575e-9\r\n```\r\n\r\nThere are a few extension methods that allow you to turn a number into a ByteSize instance:\r\n\r\n```csharp\r\n3.Bits();\r\n5.Bytes();\r\n(10.5).Kilobytes();\r\n(2.5).Megabytes();\r\n(10.2).Gigabytes();\r\n(4.7).Terabytes();\r\n```\r\n\r\nYou can also add/subtract the values using +/- operators and Add/Subtract methods:\r\n\r\n```csharp\r\nvar total = (10).Gigabytes() + (512).Megabytes() - (2.5).Gigabytes();\r\ntotal.Subtract((2500).Kilobytes()).Add((25).Megabytes());\r\n```\r\n\r\nA `ByteSize` object contains two properties that represent the largest metric prefix symbol and value:\r\n\r\n```csharp\r\nvar maxFileSize = (10).Kilobytes();\r\n\r\nmaxFileSize.LargestWholeNumberSymbol;  // \"KB\"\r\nmaxFileSize.LargestWholeNumberValue;   // 10\r\n```\r\n\r\nIf you want a string representation you can call `ToString` or `Humanize` interchangeably on the `ByteSize` instance:\r\n\r\n```csharp\r\n7.Bits().ToString();           // 7 b\r\n8.Bits().ToString();           // 1 B\r\n(.5).Kilobytes().Humanize();   // 512 B\r\n(1000).Kilobytes().ToString(); // 1000 KB\r\n(1024).Kilobytes().Humanize(); // 1 MB\r\n(.5).Gigabytes().Humanize();   // 512 MB\r\n(1024).Gigabytes().ToString(); // 1 TB\r\n```\r\n\r\nYou can also optionally provide a format for the expected string representation.\r\nThe formatter can contain the symbol of the value to display: `b`, `B`, `KB`, `MB`, `GB`, `TB`.\r\nThe formatter uses the built in [`double.ToString` method](https://docs.microsoft.com/dotnet/api/system.double.tostring) with `#.##` as the default format which rounds the number to two decimal places:\r\n\r\n```csharp\r\nvar b = (10.505).Kilobytes();\r\n\r\n// Default number format is #.##\r\nb.ToString(\"KB\");         // 10.52 KB\r\nb.Humanize(\"MB\");         // .01 MB\r\nb.Humanize(\"b\");          // 86057 b\r\n\r\n// Default symbol is the largest metric prefix value \u003e= 1\r\nb.ToString(\"#.#\");        // 10.5 KB\r\n\r\n// All valid values of double.ToString(string format) are acceptable\r\nb.ToString(\"0.0000\");     // 10.5050 KB\r\nb.Humanize(\"000.00\");     // 010.51 KB\r\n\r\n// You can include number format and symbols\r\nb.ToString(\"#.#### MB\");  // .0103 MB\r\nb.Humanize(\"0.00 GB\");    // 0 GB\r\nb.Humanize(\"#.## B\");     // 10757.12 B\r\n```\r\n\r\nIf you want a string representation with full words you can call `ToFullWords` on the `ByteSize` instance:\r\n\r\n```csharp\r\n7.Bits().ToFullWords();           // 7 bits\r\n8.Bits().ToFullWords();           // 1 byte\r\n(.5).Kilobytes().ToFullWords();   // 512 bytes\r\n(1000).Kilobytes().ToFullWords(); // 1000 kilobytes\r\n(1024).Kilobytes().ToFullWords(); // 1 megabyte\r\n(.5).Gigabytes().ToFullWords();   // 512 megabytes\r\n(1024).Gigabytes().ToFullWords(); // 1 terabyte\r\n```\r\n\r\nThere isn't a `Dehumanize` method to turn a string representation back into a `ByteSize` instance; but you can use `Parse` and `TryParse` on `ByteSize` to do that.\r\nLike other `TryParse` methods, `ByteSize.TryParse` returns `boolean` value indicating whether the parsing was successful.\r\nIf the value is parsed it is output to the `out` parameter supplied:\r\n\r\n```csharp\r\nByteSize output;\r\nByteSize.TryParse(\"1.5mb\", out output);\r\n\r\n// Invalid\r\nByteSize.Parse(\"1.5 b\");   // Can't have partial bits\r\n\r\n// Valid\r\nByteSize.Parse(\"5b\");\r\nByteSize.Parse(\"1.55B\");\r\nByteSize.Parse(\"1.55KB\");\r\nByteSize.Parse(\"1.55 kB \"); // Spaces are trimmed\r\nByteSize.Parse(\"1.55 kb\");\r\nByteSize.Parse(\"1.55 MB\");\r\nByteSize.Parse(\"1.55 mB\");\r\nByteSize.Parse(\"1.55 mb\");\r\nByteSize.Parse(\"1.55 GB\");\r\nByteSize.Parse(\"1.55 gB\");\r\nByteSize.Parse(\"1.55 gb\");\r\nByteSize.Parse(\"1.55 TB\");\r\nByteSize.Parse(\"1.55 tB\");\r\nByteSize.Parse(\"1.55 tb\");\r\n```\r\n\r\nFinally, if you need to calculate the rate at which a quantity of bytes has been transferred, you can use the `Per` method of `ByteSize`. The `Per` method accepts one argument - the measurement interval for the bytes; this is the amount of time it took to transfer the bytes.\r\n\r\nThe `Per` method returns a `ByteRate` class which has a `Humanize` method. By default, rates are given in seconds (eg, MB/s). However, if desired, a TimeUnit may be passed to `Humanize` for an alternate interval. Valid intervals are `TimeUnit.Second`, `TimeUnit.Minute`, and `TimeUnit.Hour`. Examples of each interval and example byte rate usage is below.\r\n\r\n```csharp\r\nvar size = ByteSize.FromMegabytes(10);\r\nvar measurementInterval = TimeSpan.FromSeconds(1);\r\n\r\nvar text = size.Per(measurementInterval).Humanize();\r\n// 10 MB/s\r\n\r\ntext = size.Per(measurementInterval).Humanize(TimeUnit.Minute);\r\n// 600 MB/min\r\n\r\ntext = size.Per(measurementInterval).Humanize(TimeUnit.Hour);\r\n// 35.15625 GB/hour\r\n```\r\n\r\nYou can specify a format for the bytes part of the humanized output:\r\n\r\n```csharp\r\n19854651984.Bytes().Per(1.Seconds()).Humanize(\"#.##\");\r\n// 18.49 GB/s\r\n```\r\n\r\n\r\n### Heading to words\r\n\r\nHumanizer includes methods to change a numeric heading to words. The heading can be a `double` whereas the result will be a string. You can choose whether to return a full representation of the heading (e.g. north, east, south or west), a short representation (e.g. N, E, S, W) or a Unicode arrow character (e.g. ↑, →, ↓, ←).\r\n\r\n```csharp\r\n360.ToHeading();\r\n// N\r\n720.ToHeading();\r\n// N\r\n```\r\n\r\nTo retrieve a full version of the heading, use the following call:\r\n\r\n```csharp\r\n180.ToHeading(HeadingStyle.Full);\r\n// south\r\n360.ToHeading(HeadingStyle.Full);\r\n// north\r\n```\r\n\r\nPlease note that a textual representation has a maximum deviation of 11.25°.\r\n\r\nThe methods above all have an overload with which you can provide a `CultureInfo` object in order to determine the localized result to return.\r\n\r\nTo retrieve an arrow representing the heading use the following method:\r\n\r\n```csharp\r\n90.ToHeadingArrow();\r\n// →\r\n225.ToHeadingArrow();\r\n// ↙\r\n```\r\n\r\nThe arrow representation of the heading has a maximum deviation of 22.5°.\r\n\r\nIn order to retrieve a heading based on the short text representation (e.g. N, E, S, W), the following method can be used:\r\n\r\n```csharp\r\n\"S\".FromShortHeading();\r\n// 180\r\n\"SW\".FromShortHeading();\r\n// 225\r\n```\r\n\r\n\r\n### Tupleize\r\n\r\nHumanizer can change whole numbers into their 'tuple'  using `Tupleize`. For example:\r\n\r\n```csharp\r\n1.Tupleize();\r\n// single\r\n3.Tupleize();\r\n// triple\r\n100.Tupleize();\r\n// centuple\r\n```\r\n\r\nThe numbers 1-10, 100 and 1000 will be converted into a 'named' tuple (i.e. \"single\", \"double\" etc.). Any other number \"n\" will be converted to \"n-tuple\".\r\n\r\n\r\n### Time unit to symbol\r\n\r\nHumanizer can translate time units to their symbols:\r\n\r\n```csharp\r\nTimeUnit.Day.ToSymbol();\r\n// d\r\nTimeUnit.Week.ToSymbol();\r\n// week\r\nTimeUnit.Year.ToSymbol();\r\n// y\r\n```\r\n\r\n\r\n## Mix this into your framework to simplify your life\r\n\r\nThis is just a baseline, and you can use this to simplify your day-to-day job. For example, in Asp.Net MVC we keep chucking `Display` attribute on ViewModel properties so `HtmlHelper` can generate correct labels for us; but, just like enums, in the vast majority of cases we just need a space between the words in property name - so why not use `\"string\".Humanize` for that?!\r\n\r\nYou may find an ASP.NET MVC sample [in the code](https://github.com/Humanizr/Humanizer/tree/v2.7.9/samples/Humanizer.MvcSample) that does that (although the project is excluded from the solution file to make the NuGet package available for .NET 3.5 too).\r\n\r\nThis is achieved using a custom `DataAnnotationsModelMetadataProvider` I called [HumanizerMetadataProvider](https://github.com/Humanizr/Humanizer/blob/v2.7.9/samples/Humanizer.MvcSample/HumanizerMetadataProvider.cs). It is small enough to repeat here; so here we go:\r\n\r\n```csharp\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.ComponentModel.DataAnnotations;\r\nusing System.Linq;\r\nusing System.Web.Mvc;\r\nusing Humanizer;\r\n\r\npublic class HumanizerMetadataProvider : DataAnnotationsModelMetadataProvider\r\n{\r\n    protected override ModelMetadata CreateMetadata(\r\n        IEnumerable\u003cAttribute\u003e attributes,\r\n        Type containerType,\r\n        Func\u003cobject\u003e modelAccessor,\r\n        Type modelType,\r\n        string propertyName)\r\n    {\r\n        var propertyAttributes = attributes.ToList();\r\n        var modelMetadata = base.CreateMetadata(propertyAttributes, containerType, modelAccessor, modelType, propertyName);\r\n\r\n        if (IsTransformRequired(modelMetadata, propertyAttributes))\r\n            modelMetadata.DisplayName = modelMetadata.PropertyName.Humanize();\r\n\r\n        return modelMetadata;\r\n    }\r\n\r\n    private static bool IsTransformRequired(ModelMetadata modelMetadata, IList\u003cAttribute\u003e propertyAttributes)\r\n    {\r\n        if (string.IsNullOrEmpty(modelMetadata.PropertyName))\r\n            return false;\r\n\r\n        if (propertyAttributes.OfType\u003cDisplayNameAttribute\u003e().Any())\r\n            return false;\r\n\r\n        if (propertyAttributes.OfType\u003cDisplayAttribute\u003e().Any())\r\n            return false;\r\n\r\n        return true;\r\n    }\r\n}\r\n```\r\n\r\nThis class calls the base class to extract the metadata and then, if required, humanizes the property name.\r\nIt is checking if the property already has a `DisplayName` or `Display` attribute on it in which case the metadata provider will just honor the attribute and leave the property alone.\r\nFor other properties it will Humanize the property name. That is all.\r\n\r\nNow you need to register this metadata provider with Asp.Net MVC.\r\nMake sure you use `System.Web.Mvc.ModelMetadataProviders`, and not `System.Web.ModelBinding.ModelMetadataProviders`:\r\n\r\n```csharp\r\nModelMetadataProviders.Current = new HumanizerMetadataProvider();\r\n```\r\n\r\n... and now you can replace:\r\n\r\n```csharp\r\npublic class RegisterModel\r\n{\r\n    [Display(Name = \"User name\")]\r\n    public string UserName { get; set; }\r\n\r\n    [Display(Name = \"Email address\")]\r\n    public string EmailAddress { get; set; }\r\n\r\n    [Display(Name = \"Confirm password\")]\r\n    public string ConfirmPassword { get; set; }\r\n}\r\n```\r\n\r\nwith:\r\n\r\n```csharp\r\npublic class RegisterModel\r\n{\r\n    public string UserName { get; set; }\r\n    public string EmailAddress { get; set; }\r\n    public string ConfirmPassword { get; set; }\r\n}\r\n```\r\n\r\n... and the \"metadata humanizer\" will take care of the rest.\r\n\r\nNo need to mention that if you want title casing for your labels you can chain the method with `Transform`:\r\n\r\n```csharp\r\nmodelMetadata.DisplayName = modelMetadata.PropertyName.Humanize().Transform(To.TitleCase);\r\n```\r\n\r\n\r\n## Use in ASP.NET 4.x MVC Views\r\n\r\nHumanizer is a Portable Class Library. There is currently [an issue](https://stackoverflow.com/questions/16675171/what-does-the-web-config-compilation-assemblies-element-do) if you try to use PCL's in an MVC view since the MVC views do not share the same build system as the regular project. You must specify all references in the `web.config` file, including ones the project system normally automatically adds.\r\n\r\nIf you encounter errors saying that you must add a reference to either `System.Runtime` or `System.Globalization`, this applies to you. The solution is to add the contract references to your `web.config` as listed [here](https://stackoverflow.com/a/19942274/738188). Note that this applies to any PCL you use in an MVC view, not just Humanizer.\r\n\r\n\r\n## Continuous Integration from Azure DevOps\r\n\r\nThe Humanizer project is built \u0026 tested continuously by Azure DevOps (more details [here](https://dev.azure.com/dotnet/Humanizer/_build?definitionId=14)). That applies to pull requests too. Shortly after you submit a PR you can check the build and test status notification on your PR.\r\n\r\nThe current build status on the CI server is [![Build status](https://dev.azure.com/dotnet/Humanizer/_apis/build/status/Humanizer-CI?branchName=main)](https://dev.azure.com/dotnet/Humanizer/_build?definitionId=14)\r\n\r\n## Related projects\r\n\r\nBelow is a list of related open-source projects:\r\n\r\n\r\n### Humanizer ReSharper Annotations\r\n\r\nIf using ReSharper, annotations for Humanizer are available in the [Humanizer.Annotations package](https://resharper-plugins.jetbrains.com/packages/Humanizer.Annotations/), which you can obtain via the ReSharper Extension Manager.\r\nThese annotations do not yet cover the entire library, but [pull requests are always welcome!](https://github.com/enduracode/humanizer-annotations).\r\n\r\n\r\n### PowerShell Humanizer\r\n\r\n[PowerShell Humanizer](https://github.com/dfinke/PowerShellHumanizer) is a PowerShell module that wraps Humanizer.\r\n\r\n\r\n### Humanizer JVM\r\n\r\n[Humanizer.jvm](https://github.com/MehdiK/Humanizer.jvm) is an adaptation of the Humanizer framework for .Net which is made for the jvm and is written in Kotlin.\r\nHumanizer.jvm meets all your jvm needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities.\r\n\r\n\r\n### Humanizer.node\r\n\r\n[Humanizer.node](https://github.com/fakoua/humanizer.node) is a TypeScript port of the Humanizer framework.\r\n\r\n\r\n## Contributing\r\n\r\nWe welcome contributions! If you'd like to contribute to Humanizer:\r\n\r\n- **Found a bug?** Please [open an issue](https://github.com/Humanizr/Humanizer/issues/new)\r\n- **Have a feature idea?** Create an issue to discuss it\r\n- **Want to submit code?** Read our [contribution guidelines](.github/CONTRIBUTING.md) and submit a pull request\r\n\r\nPlease read our [Code of Conduct](.github/CODE_OF_CONDUCT.md) before contributing.\r\n\r\n\r\n## License\r\n\r\nHumanizer is licensed under the [MIT License](license.txt).\r\n\r\nCopyright (c) .NET Foundation and Contributors\r\n\r\n\r\n## Reporting Issues\r\n\r\nIf you encounter any bugs or have feature requests, please:\r\n\r\n1. Check if the issue already exists in our [issue tracker](https://github.com/Humanizr/Humanizer/issues)\r\n2. If not, [create a new issue](https://github.com/Humanizr/Humanizer/issues/new) with:\r\n   - A clear description of the problem or feature request\r\n   - Steps to reproduce (for bugs)\r\n   - Expected vs actual behavior (for bugs)\r\n   - Your environment details (OS, .NET version, Humanizer version)\r\n\r\n\r\n## Icon\r\n\r\nThe icon was created by [Tyrone Rieschiek](https://twitter.com/Inkventive)\r\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhumanizr%2Fhumanizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhumanizr%2Fhumanizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhumanizr%2Fhumanizer/lists"}