├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mailmap ├── Cargo.lock ├── Cargo.toml ├── changelog.md ├── license-apache.txt ├── license-mit.txt ├── readme.md ├── rust-version.sh ├── src ├── ballot_box.rs ├── digits.rs ├── fraction.rs ├── integer.rs ├── lib.rs ├── outlined.rs ├── roman.rs ├── seven_segment.rs ├── sub_superscript.rs └── tally_marks.rs └── unlicense.txt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | rust-version: 14 | name: Rust Version 15 | runs-on: ubuntu-latest 16 | outputs: 17 | rust-version: ${{ steps.rust-version.outputs.rust-version }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Determine Rust Version 21 | id: rust-version 22 | run: echo "rust-version=$(./rust-version.sh)" >> "$GITHUB_OUTPUT" 23 | build: 24 | needs: rust-version 25 | name: ${{ matrix.min-rust-version && format('Build (Rust {0})', needs.rust-version.outputs.rust-version) || 'Build' }} 26 | strategy: 27 | matrix: 28 | min-rust-version: [true, false] 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Use Minimum Rust Version 33 | if: matrix.min-rust-version 34 | run: rustup override set "${{ needs.rust-version.outputs.rust-version }}" 35 | - name: Build 36 | run: cargo build --workspace 37 | test: 38 | name: Test 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Run tests 43 | run: cargo test --workspace 44 | lint: 45 | name: Lint 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Check Format 50 | run: cargo fmt --check --all 51 | - name: Clippy 52 | run: cargo clippy --workspace -- -Dwarnings 53 | - name: Rustdoc 54 | run: cargo doc --no-deps --workspace --all-features 55 | env: 56 | RUSTDOCFLAGS: '-Dwarnings' 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Tau Gärtli <4602612+bash@users.noreply.github.com> 2 | Tau Gärtli 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "fmtastic" 7 | version = "0.2.1" 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fmtastic" 3 | description = "Format numbers using the unicode characters via the Display trait." 4 | version = "0.2.1" 5 | rust-version = "1.75.0" 6 | edition = "2021" 7 | authors = ["Tau"] 8 | license = "Unlicense OR MIT OR Apache-2.0" 9 | readme = "readme.md" 10 | repository = "https://github.com/bash/fmtastic" 11 | homepage = "https://github.com/bash/fmtastic" 12 | keywords = ["unicode", "formatting", "fraction", "subscript", "superscript"] 13 | categories = ["value-formatting", "encoding", "no-std"] 14 | exclude = [".github/", ".mailmap", ".gitignore", "*.sh"] 15 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 0.2.1 3 | * Added support for `#![no_std]`. 4 | * Added support for 128-bit integers. 5 | * Added ballot box formatting for booleans. 6 | 7 | ## 0.2.0 8 | * Integers types are now abstracted behind an `Integer` trait. 9 | * Added support for formatting sub- and superscript as binary. 10 | * Added seven segment formatting. 11 | * Added tally marks. 12 | * Breaking: Align behaviour of + with that of the built-in formatting for `VulgarFraction`. 13 | * Breaking: Raise MSRV to 1.75.0. 14 | 15 | ## 0.1.0 16 | Initial release 17 | -------------------------------------------------------------------------------- /license-apache.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /license-mit.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fmtastic ✨ 2 | 3 | [![Docs](https://img.shields.io/docsrs/fmtastic/latest)](https://docs.rs/fmtastic) 4 | [![Crate Version](https://img.shields.io/crates/v/fmtastic)](https://crates.io/crates/fmtastic) 5 | 6 | A **fantastic**, `#![no_std]`-friendly crate for **fmt**ing numbers using the appropriate unicode characters via the [`Display`] trait. ✨ 7 | Format as vulgar fractions, super- and subscript and more. 8 | 9 | Contributions are welcome for more formats. 10 | 11 | ## Features 12 | 13 | ### [Vulgar Fractions] 14 | Creates beautiful unicode fractions like ¼ or ¹⁰⁄₃. 15 | ```rust 16 | use fmtastic::VulgarFraction; 17 | 18 | assert_eq!("¹⁰⁄₃", format!("{}", VulgarFraction::new(10, 3))); 19 | assert_eq!("¼", format!("{}", VulgarFraction::new(1, 4))); 20 | ``` 21 | 22 | ### Sub- and superscript 23 | Formats integers as sub- or superscript. 24 | 25 | ```rust 26 | use fmtastic::{Subscript, Superscript}; 27 | 28 | assert_eq!("x₁", format!("x{}", Subscript(1))); 29 | assert_eq!("n²", format!("n{}", Superscript(2))); 30 | ``` 31 | 32 | ### Roman Numerals 33 | Formats unsigned integers as Roman numerals. 34 | 35 | ```rust 36 | use fmtastic::Roman; 37 | 38 | assert_eq!("ⅾⅽⅽⅼⅹⅹⅹⅰⅹ", format!("{:#}", Roman::new(789_u16).unwrap())); // lowercase 39 | assert_eq!("ⅯⅯⅩⅩⅠⅤ", format!("{}", Roman::new(2024_u16).unwrap())); 40 | assert_eq!("MMXXIV", format!("{}", Roman::new(2024_u16).unwrap().ascii())); // ascii 41 | assert_eq!("ⅠⅠⅠ", format!("{}", Roman::from(3_u8))); // u8's can always be formatted as Roman numeral 42 | ``` 43 | 44 | ### Seven-Segment Digits 45 | Formats an unsigned integer using seven-segment digits 46 | from the [Legacy Computing] block. 47 | 48 | ```rust 49 | use fmtastic::Segmented; 50 | 51 | assert_eq!("🯶🯲🯸", format!("{}", Segmented(628_u32))); 52 | ``` 53 | 54 | ### Outlined 55 | Formats an unsigned integer using outlined digits 56 | from the [Legacy Computing Supplement] block. 57 | 58 | ```rust 59 | use fmtastic::Outlined; 60 | 61 | assert_eq!("𜳶𜳲𜳸", format!("{}", Outlined(628_u32))); 62 | ``` 63 | 64 | ### Tally Marks 65 | Formats an unsigned integer as tally marks. 66 | ```rust 67 | use fmtastic::TallyMarks; 68 | 69 | assert_eq!("𝍷𝍷𝍷", TallyMarks(3_u32).to_string()); 70 | assert_eq!("𝍸𝍸𝍷𝍷", TallyMarks(12_u32).to_string()); 71 | ``` 72 | 73 | ### Ballot Box 74 | Formats a boolean as a ballot box. 75 | 76 | ```rust 77 | use fmtastic::BallotBox; 78 | 79 | assert_eq!("☑ Buy bread", format!("{} Buy bread", BallotBox(true))); 80 | assert_eq!("☐ Do the dishes", format!("{} Do the dishes", BallotBox(false))); 81 | assert_eq!("☒ Laundry", format!("{:#} Laundry", BallotBox(true))); 82 | ``` 83 | 84 | ## [Docs](https://docs.rs/fmtastic) 85 | 86 | ## License 87 | Licensed under either of 88 | 89 | * Unlicense 90 | ([unlicense.txt](unlicense.txt) or ) 91 | * Apache License, Version 2.0 92 | ([license-apache.txt](license-apache.txt) or ) 93 | * MIT license 94 | ([license-mit.txt](license-mit.txt) or ) 95 | 96 | at your option. 97 | 98 | ## Contribution 99 | Unless you explicitly state otherwise, any contribution intentionally submitted 100 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 101 | multi-licensed as above, without any additional terms or conditions. 102 | 103 | 104 | [Legacy Computing]: https://www.unicode.org/charts/PDF/U1FB00.pdf 105 | [Legacy Computing Supplement]: https://www.unicode.org/charts/PDF/U1CC00.pdf 106 | [Vulgar Fractions]: https://en.wikipedia.org/wiki/Fraction_(mathematics)#Simple,_common,_or_vulgar_fractions 107 | [`Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html 108 | -------------------------------------------------------------------------------- /rust-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cargo metadata --format-version 1 --no-deps | jq -r '.packages | first | .rust_version' 6 | -------------------------------------------------------------------------------- /src/ballot_box.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | /// Formats a boolean as either a checked or unchecked ballot box. 4 | /// ``` 5 | /// # use fmtastic::BallotBox; 6 | /// assert_eq!("☑ Buy bread", format!("{} Buy bread", BallotBox(true))); 7 | /// assert_eq!("☐ Do the dishes", format!("{} Do the dishes", BallotBox(false))); 8 | /// assert_eq!("☒ Laundry", format!("{:#} Laundry", BallotBox(true))); 9 | /// ``` 10 | /// 11 | /// ## Formatting Flags 12 | /// ### Alternate `#` 13 | /// By default a ballot box with a check (`☑`) is used. 14 | /// The alternate flag `#` can be used to use a ballot box with an x instead (`☒`). 15 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 16 | pub struct BallotBox(pub bool); 17 | 18 | impl fmt::Display for BallotBox { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | if self.0 && f.alternate() { 21 | write!(f, "☒") 22 | } else if self.0 { 23 | write!(f, "☑") 24 | } else { 25 | write!(f, "☐") 26 | } 27 | } 28 | } 29 | 30 | impl From for BallotBox { 31 | fn from(value: bool) -> Self { 32 | BallotBox(value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/digits.rs: -------------------------------------------------------------------------------- 1 | use crate::integer::{Base, IntegerImpl}; 2 | 3 | /// Iterates the digits of the given integer. Zero has one digit. 4 | pub(crate) fn iter_digits>(n: T) -> impl Iterator { 5 | let n = n.abs(); 6 | B::powers(n).scan(n, move |remainder, power| { 7 | let digit = *remainder / power; 8 | *remainder = n % power; 9 | Some(digit.as_usize()) 10 | }) 11 | } 12 | 13 | #[cfg(test)] 14 | mod test { 15 | use super::*; 16 | use crate::integer::IntegerImpl; 17 | 18 | #[test] 19 | fn zero_has_zero_as_digits() { 20 | let digits: Vec<_> = iter_digits::<_, ::BaseTen>(0_u32).collect(); 21 | assert_eq!(vec![0], digits); 22 | } 23 | 24 | #[test] 25 | fn iterates_digits_in_base_10() { 26 | let digits: Vec<_> = 27 | iter_digits::<_, ::BaseTen>(1234567890_u32).collect(); 28 | assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0], digits); 29 | } 30 | 31 | #[test] 32 | fn iterates_digits_in_base_2() { 33 | let digits: Vec<_> = iter_digits::<_, ::BaseTwo>(0b10110110).collect(); 34 | assert_eq!(vec![1, 0, 1, 1, 0, 1, 1, 0], digits); 35 | } 36 | 37 | #[test] 38 | fn iterates_digits_of_negative_number() { 39 | let digits: Vec<_> = iter_digits::<_, ::BaseTen>(-1234).collect(); 40 | assert_eq!(vec![1, 2, 3, 4], digits); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/fraction.rs: -------------------------------------------------------------------------------- 1 | use crate::integer::{IntegerImpl, Sign}; 2 | use crate::Integer; 3 | use crate::{Subscript, Superscript}; 4 | use core::fmt::{self, Write}; 5 | 6 | /// A [Vulgar Fraction] that can be formatted as a unicode fraction using the [`Display`][`core::fmt::Display`] trait. 7 | /// 8 | /// [`Display`][`core::fmt::Display`] is implemented for all common number types. 9 | /// 10 | /// ## Formatting Flags 11 | /// ### Alternate `#` 12 | /// By default [single character fractions] are used when possible. 13 | /// This can be disabled by using the alternate flag (`#`). 14 | /// 15 | /// ### Sign: `+` 16 | /// Use the `+` flag to move the sign to the outside of the fraction 17 | /// and to always show the sign, even for positive numbers. 18 | /// 19 | /// ## Examples 20 | /// ``` 21 | /// # use fmtastic::VulgarFraction; 22 | /// assert_eq!("¹⁰⁄₃", format!("{}", VulgarFraction::new(10, 3))); 23 | /// assert_eq!("¼", format!("{}", VulgarFraction::new(1, 4))); 24 | /// 25 | /// // Sign in front of fraction 26 | /// assert_eq!("+¹⁰⁄₃", format!("{:+}", VulgarFraction::new(10, 3))); 27 | /// assert_eq!("+¹⁰⁄₃", format!("{:+}", VulgarFraction::new(-10, -3))); 28 | /// assert_eq!("-¹⁰⁄₃", format!("{:+}", VulgarFraction::new(-10, 3))); 29 | /// assert_eq!("-¹⁰⁄₃", format!("{:+}", VulgarFraction::new(10, -3))); 30 | /// assert_eq!("-¹⁄₀", format!("{:+}", VulgarFraction::new(-1, 0))); 31 | /// assert_eq!("-⁰⁄₁", format!("{:+}", VulgarFraction::new(0, -1))); 32 | /// 33 | /// // No single character fraction 34 | /// assert_eq!("¹⁄₄", format!("{:#}", VulgarFraction::new(1, 4))); 35 | /// ``` 36 | /// 37 | /// [Vulgar Fraction]: https://en.wikipedia.org/wiki/Fraction_(mathematics)#Simple,_common,_or_vulgar_fractions 38 | /// [single character fractions]: http://unicodefractions.com 39 | #[derive(Debug, Clone, Eq, PartialEq)] 40 | pub struct VulgarFraction { 41 | /// The number displayed above the fraction line. 42 | pub numerator: T, 43 | /// The number displayed below the fraction line. 44 | pub denominator: T, 45 | } 46 | 47 | impl VulgarFraction { 48 | /// Creates a new fraction from a numerator and denominator. 49 | pub const fn new(numerator: T, denominator: T) -> Self { 50 | Self { 51 | numerator, 52 | denominator, 53 | } 54 | } 55 | } 56 | 57 | impl From<(T, T)> for VulgarFraction { 58 | fn from((numerator, denominator): (T, T)) -> Self { 59 | VulgarFraction { 60 | numerator, 61 | denominator, 62 | } 63 | } 64 | } 65 | 66 | impl From for VulgarFraction 67 | where 68 | T: Integer, 69 | { 70 | fn from(value: T) -> Self { 71 | VulgarFraction::new(value, ::ONE.into_public()) 72 | } 73 | } 74 | 75 | impl fmt::Display for VulgarFraction 76 | where 77 | T: Integer, 78 | { 79 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 80 | let (sign, numerator, denominator) = 81 | extract_sign(self.numerator.into_impl(), self.denominator.into_impl(), f); 82 | 83 | if let Some(sign) = sign { 84 | f.write_char(sign)?; 85 | } 86 | 87 | if let Some(frac) = (!f.alternate()) 88 | .then(|| find_single_character_fraction(numerator, denominator)) 89 | .flatten() 90 | { 91 | f.write_char(frac) 92 | } else { 93 | write!(f, "{}", Superscript(numerator.into_public()))?; 94 | const FRACTION_SLASH: char = '\u{2044}'; 95 | f.write_char(FRACTION_SLASH)?; 96 | write!(f, "{}", Subscript(denominator.into_public())) 97 | } 98 | } 99 | } 100 | 101 | fn extract_sign(numerator: T, denominator: T, f: &fmt::Formatter) -> (Option, T, T) 102 | where 103 | T: IntegerImpl, 104 | { 105 | match numerator.sign() * denominator.sign() { 106 | Sign::PositiveOrZero if f.sign_plus() => (Some('+'), numerator.abs(), denominator.abs()), 107 | Sign::Negative if f.sign_plus() => (Some('-'), numerator.abs(), denominator.abs()), 108 | _ => (None, numerator, denominator), 109 | } 110 | } 111 | 112 | fn find_single_character_fraction(numerator: N, denominator: N) -> Option 113 | where 114 | N: TryInto, 115 | { 116 | match (numerator.try_into().ok()?, denominator.try_into().ok()?) { 117 | (1u8, 4u8) => Some('\u{bc}'), 118 | (1u8, 2u8) => Some('\u{bd}'), 119 | (3u8, 4u8) => Some('\u{be}'), 120 | (1u8, 7u8) => Some('\u{2150}'), 121 | (1u8, 9u8) => Some('\u{2151}'), 122 | (1u8, 10u8) => Some('\u{2152}'), 123 | (1u8, 3u8) => Some('\u{2153}'), 124 | (2u8, 3u8) => Some('\u{2154}'), 125 | (1u8, 5u8) => Some('\u{2155}'), 126 | (2u8, 5u8) => Some('\u{2156}'), 127 | (3u8, 5u8) => Some('\u{2157}'), 128 | (4u8, 5u8) => Some('\u{2158}'), 129 | (1u8, 6u8) => Some('\u{2159}'), 130 | (5u8, 6u8) => Some('\u{215a}'), 131 | (1u8, 8u8) => Some('\u{215b}'), 132 | (3u8, 8u8) => Some('\u{215c}'), 133 | (5u8, 8u8) => Some('\u{215d}'), 134 | (7u8, 8u8) => Some('\u{215e}'), 135 | (0u8, 3u8) => Some('\u{2189}'), 136 | _ => None, 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/integer.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use core::ops::{Div, Rem, Sub}; 3 | use core::ops::{Mul, SubAssign}; 4 | 5 | pub(crate) trait IntegerImpl 6 | where 7 | Self: Copy, 8 | Self: Div, 9 | Self: Rem, 10 | Self: TryInto, 11 | Self: TryFrom, 12 | Self: PartialOrd, 13 | Self: Sub, 14 | Self: SubAssign, 15 | { 16 | const ZERO: Self; 17 | const ONE: Self; 18 | const FIVE: Self; 19 | 20 | type Public: crate::Integer; 21 | type BaseTwo: Base; 22 | type BaseTen: Base; 23 | type BaseSixteen: Base; 24 | 25 | fn range(from: Self, to: Self) -> impl DoubleEndedIterator; 26 | 27 | fn sign(self) -> Sign { 28 | if self >= Self::ZERO { 29 | Sign::PositiveOrZero 30 | } else { 31 | Sign::Negative 32 | } 33 | } 34 | 35 | fn abs(self) -> Self; 36 | 37 | fn as_usize(self) -> usize; 38 | 39 | fn pow(self, exp: u32) -> Self; 40 | 41 | fn into_public(self) -> Self::Public; 42 | } 43 | 44 | #[allow(dead_code)] // This is clearly used dear compiler 45 | pub(crate) trait UnsignedIntegerImpl: IntegerImpl + crate::roman::RomanInteger {} 46 | 47 | pub(crate) enum Sign { 48 | Negative, 49 | PositiveOrZero, 50 | } 51 | 52 | impl Mul for Sign { 53 | type Output = Sign; 54 | 55 | fn mul(self, rhs: Self) -> Self::Output { 56 | use Sign::*; 57 | match (self, rhs) { 58 | (Negative, Negative) => PositiveOrZero, 59 | (Negative, PositiveOrZero) => Negative, 60 | (PositiveOrZero, Negative) => Negative, 61 | (PositiveOrZero, PositiveOrZero) => PositiveOrZero, 62 | } 63 | } 64 | } 65 | 66 | #[derive(Debug)] 67 | pub(crate) struct Ten; 68 | 69 | #[derive(Debug)] 70 | pub(crate) struct Two; 71 | 72 | #[derive(Debug)] 73 | pub(crate) struct Sixteen; 74 | 75 | pub(crate) trait Base: fmt::Debug { 76 | const VALUE: I; 77 | 78 | fn ilog(x: I) -> u32; 79 | 80 | fn powers(x: I) -> impl Iterator { 81 | let largest_exp = if x == I::ZERO { 0 } else { Self::ilog(x) }; 82 | (0..=largest_exp).rev().map(|e| Self::VALUE.pow(e)) 83 | } 84 | } 85 | 86 | macro_rules! common_integer_items { 87 | ($ty:ty) => { 88 | const ZERO: Self = 0; 89 | const ONE: Self = 1; 90 | const FIVE: Self = 5; 91 | 92 | type Public = $ty; 93 | type BaseTwo = Two; 94 | type BaseTen = Ten; 95 | type BaseSixteen = Sixteen; 96 | 97 | fn range(from: Self, to: Self) -> impl DoubleEndedIterator { 98 | from..to 99 | } 100 | 101 | fn as_usize(self) -> usize { 102 | self as usize 103 | } 104 | 105 | fn pow(self, exp: u32) -> Self { 106 | self.pow(exp) 107 | } 108 | 109 | fn into_public(self) -> Self::Public { 110 | self 111 | } 112 | }; 113 | } 114 | 115 | macro_rules! impl_bases { 116 | ($ty:ty) => { 117 | impl Base<$ty> for Two { 118 | const VALUE: $ty = 2; 119 | 120 | fn ilog(x: $ty) -> u32 { 121 | x.ilog2() 122 | } 123 | } 124 | 125 | impl Base<$ty> for Ten { 126 | const VALUE: $ty = 10; 127 | 128 | fn ilog(x: $ty) -> u32 { 129 | x.ilog10() 130 | } 131 | } 132 | 133 | impl Base<$ty> for Sixteen { 134 | const VALUE: $ty = 16; 135 | 136 | fn ilog(x: $ty) -> u32 { 137 | x.ilog(Self::VALUE) 138 | } 139 | } 140 | }; 141 | } 142 | 143 | macro_rules! impl_unsigned_integer { 144 | ($($ty:ty),+) => { 145 | $( 146 | impl crate::Integer for $ty {} 147 | impl crate::UnsignedInteger for $ty {} 148 | 149 | impl crate::ToIntegerImpl for $ty { 150 | type Impl = $ty; 151 | 152 | fn into_impl(self) -> $ty { 153 | self 154 | } 155 | } 156 | 157 | impl crate::ToUnsignedIntegerImpl for $ty { 158 | type UnsignedImpl = $ty; 159 | } 160 | 161 | impl UnsignedIntegerImpl for $ty {} 162 | 163 | impl IntegerImpl for $ty { 164 | common_integer_items!($ty); 165 | 166 | fn abs(self) -> Self { 167 | self 168 | } 169 | } 170 | 171 | impl_bases!($ty); 172 | )+ 173 | }; 174 | } 175 | 176 | macro_rules! impl_signed_integer { 177 | ($($ty:ty),+) => { 178 | $( 179 | impl crate::Integer for $ty {} 180 | impl crate::SignedInteger for $ty {} 181 | 182 | impl crate::ToIntegerImpl for $ty { 183 | type Impl = $ty; 184 | 185 | fn into_impl(self) -> $ty { 186 | self 187 | } 188 | } 189 | 190 | impl IntegerImpl for $ty { 191 | common_integer_items!($ty); 192 | 193 | fn abs(self) -> Self { 194 | self.abs() 195 | } 196 | } 197 | 198 | impl_bases!($ty); 199 | )+ 200 | }; 201 | } 202 | 203 | impl_unsigned_integer!(u8, u16, u32, u64, u128, usize); 204 | 205 | impl_signed_integer!(i8, i16, i32, i64, i128, isize); 206 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A **fantastic** crate for **fmt**ing numbers using the appropriate unicode characters via the [`Display`](core::fmt::Display) trait. ✨ \ 2 | //! Supports vulgar fractions, super- and subscript. 3 | //! 4 | //! # [Vulgar Fractions] 5 | //! Creates beautiful unicode fractions like ¼ or ¹⁰⁄₃. 6 | //! ``` 7 | //! # use fmtastic::VulgarFraction; 8 | //! assert_eq!("¹⁰⁄₃", format!("{}", VulgarFraction::new(10, 3))); 9 | //! assert_eq!("¼", format!("{}", VulgarFraction::new(1, 4))); 10 | //! ``` 11 | //! 12 | //! # Sub- and superscript 13 | //! Formats integers as sub- or superscript. 14 | //! 15 | //! ``` 16 | //! # use fmtastic::{Subscript, Superscript}; 17 | //! assert_eq!("x₁", format!("x{}", Subscript(1))); 18 | //! assert_eq!("n²", format!("n{}", Superscript(2))); 19 | //! ``` 20 | //! 21 | //! # Roman Numerals 22 | //! Formats unsigned integers as Roman numerals. 23 | //! 24 | //! ``` 25 | //! # use fmtastic::Roman; 26 | //! assert_eq!("ⅾⅽⅽⅼⅹⅹⅹⅰⅹ", format!("{:#}", Roman::new(789_u16).unwrap())); // lowercase 27 | //! assert_eq!("ⅯⅯⅩⅩⅠⅤ", format!("{}", Roman::new(2024_u16).unwrap())); 28 | //! assert_eq!("MMXXIV", format!("{}", Roman::new(2024_u16).unwrap().ascii())); // ascii 29 | //! assert_eq!("ⅠⅠⅠ", format!("{}", Roman::from(3_u8))); // u8's can always be formatted as Roman numeral 30 | //! ``` 31 | //! 32 | //! [Vulgar Fractions]: https://en.wikipedia.org/wiki/Fraction_(mathematics)#Simple,_common,_or_vulgar_fractions 33 | //! 34 | //! # Seven-Segment Digits 35 | //! Formats an unsigned integer using seven-segment digits 36 | //! from the [Legacy Computing] block. 37 | //! ``` 38 | //! # use fmtastic::Segmented; 39 | //! assert_eq!("🯶🯲🯸", format!("{}", Segmented(628_u32))); 40 | //! ``` 41 | //! 42 | //! [Legacy Computing]: https://www.unicode.org/charts/PDF/U1FB00.pdf 43 | //! 44 | //! # Outlined 45 | //! Formats an unsigned integer using outlined digits 46 | //! from the [Legacy Computing Supplement] block. 47 | //! 48 | //! ``` 49 | //! # use fmtastic::Outlined; 50 | //! assert_eq!("𜳶𜳲𜳸", format!("{}", Outlined(628_u32))); 51 | //! ``` 52 | //! 53 | //! [Legacy Computing Supplement]: https://www.unicode.org/charts/PDF/U1CC00.pdf 54 | //! 55 | //! # Tally Marks 56 | //! Formats an unsigned integer as tally marks. 57 | //! 58 | //! ``` 59 | //! # use fmtastic::TallyMarks; 60 | //! assert_eq!("𝍷𝍷𝍷", TallyMarks(3_u32).to_string()); 61 | //! assert_eq!("𝍸𝍸𝍷𝍷", TallyMarks(12_u32).to_string()); 62 | //! ``` 63 | //! 64 | //! # Ballot Box 65 | //! Formats a boolean as a ballot box. 66 | //! 67 | //! ``` 68 | //! # use fmtastic::BallotBox; 69 | //! assert_eq!("☑ Buy bread", format!("{} Buy bread", BallotBox(true))); 70 | //! assert_eq!("☐ Do the dishes", format!("{} Do the dishes", BallotBox(false))); 71 | //! assert_eq!("☒ Laundry", format!("{:#} Laundry", BallotBox(true))); 72 | //! ``` 73 | 74 | #![forbid(unsafe_code)] 75 | #![warn(missing_docs)] 76 | #![cfg_attr(not(test), no_std)] 77 | 78 | /// An abstraction over all integer types. 79 | /// Integers can be formatted as [`Subscript`], [`Subscript`] or [`VulgarFraction`]. 80 | /// 81 | /// Use this trait if you want to abstract over integers that can be formatted 82 | /// by one of this crate's formats: 83 | /// 84 | /// ``` 85 | /// use fmtastic::{Subscript, Integer}; 86 | /// 87 | /// assert_eq!("x₁", x_with_index(1u8)); 88 | /// assert_eq!("x₅", x_with_index(5u64)); 89 | /// 90 | /// fn x_with_index(index: T) -> String { 91 | /// format!("x{}", Subscript(index)) 92 | /// } 93 | /// ``` 94 | #[allow(private_bounds)] 95 | pub trait Integer: ToIntegerImpl + Copy {} 96 | 97 | /// Abstraction over signed integer types. 98 | pub trait SignedInteger: Integer {} 99 | 100 | /// Abstraction over unsigned integer types. 101 | /// Unsigned integers can be formatted as [`Segmented`] or [`TallyMarks`]. 102 | #[allow(private_bounds)] 103 | pub trait UnsignedInteger: Integer + ToUnsignedIntegerImpl {} 104 | 105 | pub(crate) trait ToIntegerImpl { 106 | type Impl: crate::integer::IntegerImpl; 107 | 108 | fn into_impl(self) -> Self::Impl; 109 | } 110 | 111 | pub(crate) trait ToUnsignedIntegerImpl: ToIntegerImpl { 112 | type UnsignedImpl: integer::UnsignedIntegerImpl; 113 | } 114 | 115 | mod sub_superscript; 116 | pub use sub_superscript::*; 117 | mod fraction; 118 | pub use fraction::*; 119 | mod integer; 120 | mod tally_marks; 121 | pub use tally_marks::*; 122 | mod seven_segment; 123 | pub use seven_segment::*; 124 | mod ballot_box; 125 | pub use ballot_box::*; 126 | mod roman; 127 | pub use roman::*; 128 | mod outlined; 129 | pub use outlined::*; 130 | 131 | mod digits; 132 | 133 | #[doc = include_str!("../readme.md")] 134 | #[cfg(doctest)] 135 | pub mod readme_doctest {} 136 | -------------------------------------------------------------------------------- /src/outlined.rs: -------------------------------------------------------------------------------- 1 | use crate::digits::iter_digits; 2 | use crate::integer::{Base, IntegerImpl}; 3 | use crate::UnsignedInteger; 4 | use core::fmt; 5 | 6 | /// Formats an unsigned integer using outlined digits 7 | /// from the [Legacy Computing Supplement] block. 8 | /// 9 | /// You may need to install an extra font such as [Kreative Square] to display these digits. 10 | /// 11 | /// [Legacy Computing Supplement]: https://www.unicode.org/charts/PDF/U1CC00.pdf 12 | /// [Kreative Square]: http://www.kreativekorp.com/software/fonts/ksquare/ 13 | /// 14 | /// ``` 15 | /// use fmtastic::Outlined; 16 | /// 17 | /// assert_eq!("𜳶𜳲𜳸", Outlined(628_u32).to_string()); 18 | /// 19 | /// assert_eq!("𜳰", Outlined(0_u32).to_string()); 20 | /// assert_eq!("𜳱", Outlined(1_u32).to_string()); 21 | /// assert_eq!("𜳲", Outlined(2_u32).to_string()); 22 | /// assert_eq!("𜳳", Outlined(3_u32).to_string()); 23 | /// assert_eq!("𜳴", Outlined(4_u32).to_string()); 24 | /// assert_eq!("𜳵", Outlined(5_u32).to_string()); 25 | /// assert_eq!("𜳶", Outlined(6_u32).to_string()); 26 | /// assert_eq!("𜳷", Outlined(7_u32).to_string()); 27 | /// assert_eq!("𜳸", Outlined(8_u32).to_string()); 28 | /// assert_eq!("𜳹", Outlined(9_u32).to_string()); 29 | /// 30 | /// // Binary 31 | /// assert_eq!("𜳰", format!("{:b}", Outlined(0_u8))); 32 | /// assert_eq!("𜳱𜳰𜳱𜳰𜳱𜳰", format!("{:+b}", Outlined(0b101010_u8))); 33 | /// 34 | /// // Hexadecimal 35 | /// assert_eq!("𜳱𜳘𜳘𜳛𜳰", format!("{:X}", Outlined(0x1CCF0_u32))); 36 | /// ``` 37 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 38 | pub struct Outlined(pub T); 39 | 40 | impl From for Outlined 41 | where 42 | T: UnsignedInteger, 43 | { 44 | fn from(value: T) -> Self { 45 | Outlined(value) 46 | } 47 | } 48 | 49 | impl fmt::Binary for Outlined 50 | where 51 | T: UnsignedInteger, 52 | { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | fmt_outlined::<_, ::BaseTwo>(self.0.into_impl(), f) 55 | } 56 | } 57 | 58 | impl fmt::Display for Outlined 59 | where 60 | T: UnsignedInteger, 61 | { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | fmt_outlined::<_, ::BaseTen>(self.0.into_impl(), f) 64 | } 65 | } 66 | 67 | impl fmt::UpperHex for Outlined 68 | where 69 | T: UnsignedInteger, 70 | { 71 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 | fmt_outlined::<_, ::BaseSixteen>(self.0.into_impl(), f) 73 | } 74 | } 75 | 76 | fn fmt_outlined>(n: T, f: &mut fmt::Formatter<'_>) -> fmt::Result { 77 | iter_digits::<_, B>(n).try_for_each(|digit| write!(f, "{}", DIGITS[digit])) 78 | } 79 | 80 | const DIGITS: [&str; 16] = [ 81 | // Outlined digits 0-9 82 | "\u{1CCF0}", 83 | "\u{1CCF1}", 84 | "\u{1CCF2}", 85 | "\u{1CCF3}", 86 | "\u{1CCF4}", 87 | "\u{1CCF5}", 88 | "\u{1CCF6}", 89 | "\u{1CCF7}", 90 | "\u{1CCF8}", 91 | "\u{1CCF9}", 92 | // Outlined uppercase Latin alphabet A-F 93 | "\u{1CCD6}", 94 | "\u{1CCD7}", 95 | "\u{1CCD8}", 96 | "\u{1CCD9}", 97 | "\u{1CCDA}", 98 | "\u{1CCDB}", 99 | ]; 100 | -------------------------------------------------------------------------------- /src/roman.rs: -------------------------------------------------------------------------------- 1 | // Adapted from Yann Villessuzanne's roman.rs under the 2 | // Unlicense, at https://github.com/linfir/roman.rs/ 3 | 4 | use crate::integer::IntegerImpl; 5 | use crate::UnsignedInteger; 6 | use core::fmt; 7 | 8 | /// Formats unsigned integers as Roman numerals. 9 | /// 10 | /// By default, the dedicated unicode symbols for Roman numerals are used. 11 | /// You can use [`Roman::ascii`] to use ASCII symbols instead. 12 | /// 13 | /// ``` 14 | /// # use fmtastic::Roman; 15 | /// assert_eq!("ⅾⅽⅽⅼⅹⅹⅹⅰⅹ", format!("{:#}", Roman::new(789_u16).unwrap())); // lowercase 16 | /// assert_eq!("ⅯⅯⅩⅩⅠⅤ", format!("{}", Roman::new(2024_u16).unwrap())); 17 | /// assert_eq!("MMXXIV", format!("{}", Roman::new(2024_u16).unwrap().ascii())); // ascii 18 | /// assert_eq!("ⅠⅠⅠ", format!("{}", Roman::from(3_u8))); // u8's can always be formatted as Roman numeral 19 | /// ``` 20 | /// 21 | /// ## Formatting Flags 22 | /// ### Alternate `#` 23 | /// By default uppercase numerals are used. 24 | /// The alternate flag `#` can be used to switch to lowercase numerals. 25 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 26 | pub struct Roman(T, SymbolRepertoire); 27 | 28 | impl Roman { 29 | /// Uses ASCII symbols instead of the dedicated unciode 30 | /// symbols for Roman numerals. 31 | pub fn ascii(mut self) -> Self { 32 | self.1 = SymbolRepertoire::Ascii; 33 | self 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 38 | #[non_exhaustive] 39 | enum SymbolRepertoire { 40 | Unicode, 41 | Ascii, 42 | } 43 | 44 | impl From for Roman { 45 | fn from(value: u8) -> Self { 46 | Roman(value, SymbolRepertoire::Unicode) 47 | } 48 | } 49 | 50 | impl Roman 51 | where 52 | T: UnsignedInteger, 53 | { 54 | /// Creates a new [`Roman`] numeral. 55 | /// Returns `None` if the value is not between 1 and 3999. 56 | pub fn new(value: T) -> Option> { 57 | if T::Impl::ZERO < value.into_impl() && value.into_impl() <= T::UnsignedImpl::ROMAN_MAX { 58 | Some(Roman(value, SymbolRepertoire::Unicode)) 59 | } else { 60 | None 61 | } 62 | } 63 | } 64 | 65 | impl fmt::Display for Roman 66 | where 67 | T: UnsignedInteger, 68 | { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | let mut n = self.0.into_impl(); 71 | for (symbol, value) in roman_pairs::(self.1, f.alternate()) { 72 | let value = value.into_impl(); 73 | while n >= value { 74 | n -= value; 75 | write!(f, "{symbol}")?; 76 | } 77 | } 78 | debug_assert!(n == T::Impl::ZERO); 79 | Ok(()) 80 | } 81 | } 82 | 83 | fn roman_pairs( 84 | repertoire: SymbolRepertoire, 85 | lowercase: bool, 86 | ) -> impl Iterator 87 | where 88 | T: UnsignedInteger, 89 | { 90 | ROMAN_PAIRS.iter().copied().filter_map( 91 | move |(upper_unicode, lower_unicode, upper_ascii, lower_ascii, value)| { 92 | let symbol = match (repertoire, lowercase) { 93 | (SymbolRepertoire::Unicode, false) => upper_unicode, 94 | (SymbolRepertoire::Unicode, true) => lower_unicode, 95 | (SymbolRepertoire::Ascii, false) => upper_ascii, 96 | (SymbolRepertoire::Ascii, true) => lower_ascii, 97 | }; 98 | Some((symbol, T::Impl::try_from(value).ok()?.into_public())) 99 | }, 100 | ) 101 | } 102 | 103 | static ROMAN_PAIRS: &[(&str, &str, &str, &str, u16)] = &[ 104 | ("Ⅿ", "ⅿ", "M", "m", 1000), 105 | ("ⅭⅯ", "ⅽⅿ", "CM", "cm", 900), 106 | ("Ⅾ", "ⅾ", "D", "d", 500), 107 | ("ⅭⅮ", "ⅽⅾ", "CD", "cd", 400), 108 | ("Ⅽ", "ⅽ", "C", "c", 100), 109 | ("ⅩⅭ", "ⅹⅽ", "XC", "xc", 90), 110 | ("Ⅼ", "ⅼ", "L", "l", 50), 111 | ("ⅩⅬ", "ⅹⅼ", "XL", "xl", 40), 112 | ("Ⅹ", "ⅹ", "X", "x", 10), 113 | ("ⅠⅩ", "ⅰⅹ", "IX", "ix", 9), 114 | ("Ⅴ", "ⅴ", "V", "v", 5), 115 | ("ⅠⅤ", "ⅰⅴ", "IV", "iv", 4), 116 | ("Ⅰ", "ⅰ", "I", "i", 1), 117 | ]; 118 | 119 | pub(crate) trait RomanInteger { 120 | const ROMAN_MAX: Self; 121 | } 122 | 123 | impl RomanInteger for u8 { 124 | const ROMAN_MAX: Self = u8::MAX; 125 | } 126 | 127 | macro_rules! impl_roman_integer { 128 | ($($ty:ty),*) => { 129 | $( 130 | impl RomanInteger for $ty { 131 | /// The largest number representable as a roman numeral. 132 | const ROMAN_MAX: Self = 3999; 133 | } 134 | )* 135 | } 136 | } 137 | 138 | impl_roman_integer!(u16, u32, u64, u128, usize); 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use super::*; 143 | 144 | #[test] 145 | fn test_to_roman() { 146 | let roman = 147 | "I II III IV V VI VII VIII IX X XI XII XIII XIV XV XVI XVII XVIII XIX XX XXI XXII" 148 | .split(' '); 149 | for (i, x) in roman.enumerate() { 150 | let n = i + 1; 151 | assert_eq!(format!("{}", Roman::new(n).unwrap().ascii()), x); 152 | } 153 | assert_eq!( 154 | format!("{}", Roman::new(1984u32).unwrap().ascii()), 155 | "MCMLXXXIV" 156 | ); 157 | assert_eq!( 158 | format!("{}", Roman::new(448u32).unwrap().ascii()), 159 | "CDXLVIII" 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/seven_segment.rs: -------------------------------------------------------------------------------- 1 | use crate::digits::iter_digits; 2 | use crate::integer::{Base, IntegerImpl}; 3 | use crate::UnsignedInteger; 4 | use core::fmt; 5 | 6 | /// Formats an unsigned integer using seven-segment digits 7 | /// from the [Legacy Computing] block. 8 | /// 9 | /// You may need to install an extra font such as [Sieben 7], [Cascadia Code], or [Noto Sans Symbols 2] 10 | /// since most other fonts do not support these digits. 11 | /// 12 | /// [Legacy Computing]: https://www.unicode.org/charts/PDF/U1FB00.pdf 13 | /// [Sieben 7]: https://github.com/bash/sieben-7 14 | /// [Noto Sans Symbols 2]: https://fonts.google.com/noto/specimen/Noto+Sans+Symbols+2 15 | /// [Cascadia Code]: https://github.com/microsoft/cascadia-code 16 | /// 17 | /// ``` 18 | /// use fmtastic::Segmented; 19 | /// 20 | /// assert_eq!("🯶🯲🯸", Segmented(628_u32).to_string()); 21 | /// 22 | /// assert_eq!("🯰", Segmented(0_u32).to_string()); 23 | /// assert_eq!("🯱", Segmented(1_u32).to_string()); 24 | /// assert_eq!("🯲", Segmented(2_u32).to_string()); 25 | /// assert_eq!("🯳", Segmented(3_u32).to_string()); 26 | /// assert_eq!("🯴", Segmented(4_u32).to_string()); 27 | /// assert_eq!("🯵", Segmented(5_u32).to_string()); 28 | /// assert_eq!("🯶", Segmented(6_u32).to_string()); 29 | /// assert_eq!("🯷", Segmented(7_u32).to_string()); 30 | /// assert_eq!("🯸", Segmented(8_u32).to_string()); 31 | /// assert_eq!("🯹", Segmented(9_u32).to_string()); 32 | /// 33 | /// // Binary 34 | /// assert_eq!("🯰", format!("{:b}", Segmented(0_u8))); 35 | /// assert_eq!("🯱🯰🯱🯰🯱🯰", format!("{:+b}", Segmented(0b101010_u8))); 36 | /// ``` 37 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 38 | pub struct Segmented(pub T); 39 | 40 | impl From for Segmented 41 | where 42 | T: UnsignedInteger, 43 | { 44 | fn from(value: T) -> Self { 45 | Segmented(value) 46 | } 47 | } 48 | 49 | impl fmt::Binary for Segmented 50 | where 51 | T: UnsignedInteger, 52 | { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | fmt_seven_segment::<_, ::BaseTwo>(self.0.into_impl(), f) 55 | } 56 | } 57 | 58 | impl fmt::Display for Segmented 59 | where 60 | T: UnsignedInteger, 61 | { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | fmt_seven_segment::<_, ::BaseTen>(self.0.into_impl(), f) 64 | } 65 | } 66 | 67 | fn fmt_seven_segment>(n: T, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | iter_digits::<_, B>(n).try_for_each(|digit| write!(f, "{}", DIGITS[digit])) 69 | } 70 | 71 | const DIGITS: [&str; 10] = [ 72 | "\u{1FBF0}", 73 | "\u{1FBF1}", 74 | "\u{1FBF2}", 75 | "\u{1FBF3}", 76 | "\u{1FBF4}", 77 | "\u{1FBF5}", 78 | "\u{1FBF6}", 79 | "\u{1FBF7}", 80 | "\u{1FBF8}", 81 | "\u{1FBF9}", 82 | ]; 83 | -------------------------------------------------------------------------------- /src/sub_superscript.rs: -------------------------------------------------------------------------------- 1 | use crate::digits::iter_digits; 2 | use crate::integer::{Base, IntegerImpl, Sign}; 3 | use crate::Integer; 4 | use core::fmt::{self, Write}; 5 | 6 | /// A number that can be formatted as superscript using the [`Display`][`core::fmt::Display`] trait. 7 | /// 8 | /// [`Display`][`core::fmt::Display`] is implemented for all common number types. 9 | /// 10 | /// ## Formatting Flags 11 | /// ### Sign: `+` 12 | /// Use the `+` flag to always include the + sign for positive numbers. 13 | /// 14 | /// ## Examples 15 | /// ``` 16 | /// # use fmtastic::Superscript; 17 | /// assert_eq!("¹²³", format!("{}", Superscript(123))); 18 | /// assert_eq!("⁰", format!("{}", Superscript(0))); 19 | /// assert_eq!("⁻¹²³", format!("{}", Superscript(-123))); 20 | /// assert_eq!("⁺¹²³", format!("{:+}", Superscript(123))); 21 | /// 22 | /// // Binary 23 | /// assert_eq!("¹⁰¹⁰¹⁰", format!("{:b}", Superscript(0b101010))); 24 | /// assert_eq!("⁺¹⁰¹⁰¹⁰", format!("{:+b}", Superscript(0b101010))); 25 | /// assert_eq!("⁻¹⁰¹⁰¹⁰", format!("{:b}", Superscript(-0b101010))); 26 | /// ``` 27 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 28 | pub struct Superscript(pub T); 29 | 30 | impl From for Superscript 31 | where 32 | T: Integer, 33 | { 34 | fn from(value: T) -> Self { 35 | Superscript(value) 36 | } 37 | } 38 | 39 | impl fmt::Display for Superscript 40 | where 41 | T: Integer, 42 | { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | fmt_number_with_base_and_digits::<_, ::BaseTen>( 45 | f, 46 | self.0.into_impl(), 47 | '⁺', 48 | '⁻', 49 | &['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'], 50 | ) 51 | } 52 | } 53 | 54 | impl fmt::Binary for Superscript 55 | where 56 | T: Integer, 57 | { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | fmt_number_with_base_and_digits::<_, ::BaseTwo>( 60 | f, 61 | self.0.into_impl(), 62 | '⁺', 63 | '⁻', 64 | &['⁰', '¹'], 65 | ) 66 | } 67 | } 68 | 69 | /// A number that can be formatted as subscript using the [`Display`][`core::fmt::Display`] trait. 70 | /// 71 | /// [`Display`][`core::fmt::Display`] is implemented for all common number types. 72 | /// 73 | /// ## Formatting Flags 74 | /// ### Sign: `+` 75 | /// Use the `+` flag to always include the + sign for positive numbers. 76 | /// 77 | /// ## Examples 78 | /// ``` 79 | /// # use fmtastic::Subscript; 80 | /// assert_eq!("₁₂₃", format!("{}", Subscript(123))); 81 | /// assert_eq!("₀", format!("{}", Subscript(0))); 82 | /// assert_eq!("₋₁₂₃", format!("{}", Subscript(-123))); 83 | /// assert_eq!("₊₁₂₃", format!("{:+}", Subscript(123))); 84 | /// 85 | /// // Binary 86 | /// assert_eq!("₁₀₁₀₁₀", format!("{:b}", Subscript(0b101010))); 87 | /// assert_eq!("₊₁₀₁₀₁₀", format!("{:+b}", Subscript(0b101010))); 88 | /// assert_eq!("₋₁₀₁₀₁₀", format!("{:b}", Subscript(-0b101010))); 89 | /// ``` 90 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 91 | pub struct Subscript(pub T); 92 | 93 | impl From for Subscript 94 | where 95 | T: Integer, 96 | { 97 | fn from(value: T) -> Self { 98 | Subscript(value) 99 | } 100 | } 101 | 102 | impl fmt::Display for Subscript 103 | where 104 | T: Integer, 105 | { 106 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 | fmt_number_with_base_and_digits::<_, ::BaseTen>( 108 | f, 109 | self.0.into_impl(), 110 | '₊', 111 | '₋', 112 | &['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'], 113 | ) 114 | } 115 | } 116 | 117 | impl fmt::Binary for Subscript 118 | where 119 | T: Integer, 120 | { 121 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 122 | fmt_number_with_base_and_digits::<_, ::BaseTwo>( 123 | f, 124 | self.0.into_impl(), 125 | '₊', 126 | '₋', 127 | &['₀', '₁'], 128 | ) 129 | } 130 | } 131 | 132 | fn fmt_number_with_base_and_digits>( 133 | f: &mut fmt::Formatter<'_>, 134 | n: T, 135 | plus: char, 136 | minus: char, 137 | digits: &[char], 138 | ) -> fmt::Result { 139 | match n.sign() { 140 | Sign::PositiveOrZero if f.sign_plus() => f.write_char(plus)?, 141 | Sign::Negative => f.write_char(minus)?, 142 | _ => {} 143 | }; 144 | 145 | iter_digits::(n) 146 | .map(|digit| digits[digit]) 147 | .try_for_each(|digit| f.write_char(digit)) 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | 154 | #[test] 155 | fn formats_as_superscript() { 156 | for (expected, input) in [ 157 | ("⁰", 0), 158 | ("¹", 1), 159 | ("²", 2), 160 | ("³", 3), 161 | ("⁴", 4), 162 | ("⁵", 5), 163 | ("⁶", 6), 164 | ("⁷", 7), 165 | ("⁸", 8), 166 | ("⁹", 9), 167 | ("¹⁰", 10), 168 | ("¹²³⁴⁵⁶⁷⁸⁹⁰", 1234567890), 169 | ("⁻¹²³⁴⁵⁶⁷⁸⁹⁰", -1234567890), 170 | ] { 171 | assert_eq!(expected, Superscript(input).to_string()) 172 | } 173 | } 174 | 175 | #[test] 176 | fn adds_superscript_plus_sign_to_positive_numbers() { 177 | assert_eq!("⁺⁰", format!("{:+}", Superscript(0u64))); 178 | assert_eq!("⁺⁰", format!("{:+}", Superscript(0i64))); 179 | assert_eq!("⁺¹²³⁴⁵⁶⁷⁸⁹⁰", format!("{:+}", Superscript(1234567890u64))); 180 | assert_eq!("⁺¹²³⁴⁵⁶⁷⁸⁹⁰", format!("{:+}", Superscript(1234567890i64))); 181 | assert_eq!("⁻¹²³⁴⁵⁶⁷⁸⁹⁰", format!("{:+}", Superscript(-1234567890))); 182 | } 183 | 184 | #[test] 185 | fn formats_as_subscript() { 186 | for (expected, input) in [ 187 | ("₀", 0), 188 | ("₁", 1), 189 | ("₂", 2), 190 | ("₃", 3), 191 | ("₄", 4), 192 | ("₅", 5), 193 | ("₆", 6), 194 | ("₇", 7), 195 | ("₈", 8), 196 | ("₉", 9), 197 | ("₁₀", 10), 198 | ("₁₂₃₄₅₆₇₈₉₀", 1234567890), 199 | ("₋₁₂₃₄₅₆₇₈₉₀", -1234567890), 200 | ] { 201 | assert_eq!(expected, Subscript(input).to_string()) 202 | } 203 | } 204 | 205 | #[test] 206 | fn adds_subscript_plus_sign_to_positive_numbers() { 207 | assert_eq!("₊₀", format!("{:+}", Subscript(0))); 208 | assert_eq!("₊₁₂₃₄₅₆₇₈₉₀", format!("{:+}", Subscript(1234567890))); 209 | assert_eq!("₋₁₂₃₄₅₆₇₈₉₀", format!("{:+}", Subscript(-1234567890))); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/tally_marks.rs: -------------------------------------------------------------------------------- 1 | use crate::integer::IntegerImpl; 2 | use crate::UnsignedInteger; 3 | use core::fmt::{self, Write}; 4 | 5 | /// Formats an unsigned integer as tally marks. 6 | /// 7 | /// You may need to install an extra font such as [Noto Sans Symbols 2] 8 | /// since most other fonts do not support these digits. 9 | /// 10 | /// [Noto Sans Symbols 2]: https://fonts.google.com/noto/specimen/Noto+Sans+Symbols+2 11 | /// 12 | /// ``` 13 | /// use fmtastic::TallyMarks; 14 | /// 15 | /// assert_eq!("", TallyMarks(0_u32).to_string()); 16 | /// assert_eq!("𝍷", TallyMarks(1_u32).to_string()); 17 | /// assert_eq!("𝍷𝍷", TallyMarks(2_u32).to_string()); 18 | /// assert_eq!("𝍷𝍷𝍷", TallyMarks(3_u32).to_string()); 19 | /// assert_eq!("𝍷𝍷𝍷𝍷", TallyMarks(4_u32).to_string()); 20 | /// assert_eq!("𝍸", TallyMarks(5_u32).to_string()); 21 | /// assert_eq!("𝍸𝍷", TallyMarks(6_u32).to_string()); 22 | /// assert_eq!("𝍸𝍸𝍸𝍷𝍷", TallyMarks(17_u32).to_string()); 23 | /// ``` 24 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 25 | pub struct TallyMarks(pub T); 26 | 27 | impl From for TallyMarks 28 | where 29 | T: UnsignedInteger, 30 | { 31 | fn from(value: T) -> Self { 32 | TallyMarks(value) 33 | } 34 | } 35 | 36 | impl fmt::Display for TallyMarks 37 | where 38 | T: UnsignedInteger, 39 | { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | fmt_tally_marks(self.0.into_impl(), f) 42 | } 43 | } 44 | 45 | fn fmt_tally_marks(n: T, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | const TALLY_MARK_ONE: char = '\u{1D377}'; 47 | const TALLY_MARK_FIVE: char = '\u{1D378}'; 48 | let (fives, ones) = (n / T::FIVE, n % T::FIVE); 49 | T::range(T::ZERO, fives).try_for_each(|_| f.write_char(TALLY_MARK_FIVE))?; 50 | T::range(T::ZERO, ones).try_for_each(|_| f.write_char(TALLY_MARK_ONE))?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | --------------------------------------------------------------------------------