qfall_math/rational/q/to_string.rs
1// Copyright © 2023 Marcel Luca Schmidt, Marvin Beckmann
2//
3// This file is part of qFALL-math.
4//
5// qFALL-math is free software: you can redistribute it and/or modify it under
6// the terms of the Mozilla Public License Version 2.0 as published by the
7// Mozilla Foundation. See <https://mozilla.org/en-US/MPL/2.0/>.
8
9//! This module contains all options to convert a rational of type
10//! [`Q`] into a [`String`].
11//!
12//! This includes the [`Display`](std::fmt::Display) trait.
13
14use super::Q;
15use crate::macros::for_others::implement_for_owned;
16use core::fmt;
17use flint_sys::fmpq::fmpq_get_str;
18use std::{ffi::CStr, ptr::null_mut};
19
20impl From<&Q> for String {
21 /// Converts a [`Q`] into its [`String`] representation.
22 ///
23 /// Parameters:
24 /// - `value`: specifies the rational that will be represented as a [`String`]
25 ///
26 /// Returns a [`String`] of the form `"x/y"`.
27 ///
28 /// # Examples
29 /// ```
30 /// use qfall_math::rational::Q;
31 /// use std::str::FromStr;
32 /// let rational = Q::from_str("6/7").unwrap();
33 ///
34 /// let string: String = rational.into();
35 /// ```
36 fn from(value: &Q) -> Self {
37 value.to_string()
38 }
39}
40
41implement_for_owned!(Q, String, From);
42
43impl fmt::Display for Q {
44 /// Allows to convert a rational of type [`Q`] into a [`String`].
45 ///
46 /// Returns the rational in form of a [`String`]. For rational `1/2`
47 /// the String looks like this `1/2`.
48 ///
49 /// # Examples
50 /// ```
51 /// use std::str::FromStr;
52 /// use qfall_math::rational::Q;
53 /// use core::fmt;
54 ///
55 /// let rational = Q::from((-1, 235));
56 /// println!("{rational}");
57 /// ```
58 ///
59 /// ```
60 /// use std::str::FromStr;
61 /// use qfall_math::rational::Q;
62 /// use core::fmt;
63 ///
64 /// let rational = Q::from((-1, 235));
65 /// let integer_string = rational.to_string();
66 /// ```
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 let c_str_ptr = unsafe { fmpq_get_str(null_mut(), 10, &self.value) };
69
70 // we expect c_str_ptr to be reference a real value, hence get_str returns an
71 // actual value, hence a simple unwrap should be sufficient and we do not have
72 // to consider an exception
73 //
74 // c_string should not be null either, since we call this method on an
75 // instantiated object
76 let msg = "We expect the pointer to point to a real value and the c_string
77 not to be null. This error occurs if the provided string does not have UTF-8 format.";
78 let return_str = unsafe { CStr::from_ptr(c_str_ptr).to_str().expect(msg).to_owned() };
79
80 unsafe { libc::free(c_str_ptr as *mut libc::c_void) };
81
82 write!(f, "{return_str}")
83 }
84}
85
86impl Q {
87 /// Outputs the decimal representation of a [`Q`] with
88 /// the specified number of decimal digits.
89 /// If `self` can't be represented exactly, it provides the
90 /// closest value representable with `nr_decimal_digits` rounded
91 /// towards the next representable number.
92 ///
93 /// Notice that, e.g., `0.5` is represented as `0.499...` as [`f64`].
94 /// Therefore, rounding with `nr_decimal_digits = 0` will output `0`.
95 ///
96 /// **WARNING:** This function converts the [`Q`] value into an [`f64`] before
97 /// outputting the decimal representation. Thus, values that can't be represented exactly
98 /// by an [`f64`] will lose some precision. For large values, e.g. of size `2^64`
99 /// the deviation to the original value might be within the size of `1_000`.
100 ///
101 /// Parameters:
102 /// - `nr_decimal_digits`: specifies the number of decimal digits
103 /// that will be a part of the output [`String`]
104 ///
105 /// Returns a [`String`] of the form `"10.25"` if `nr_decimal_digits = 2`.
106 ///
107 /// # Examples
108 /// ```
109 /// use qfall_math::rational::Q;
110 /// use std::str::FromStr;
111 /// let rational = Q::from_str("6/7").unwrap();
112 ///
113 /// let decimal_repr = rational.to_string_decimal(3);
114 /// ```
115 pub fn to_string_decimal(&self, nr_decimal_digits: usize) -> String {
116 let value = f64::from(self);
117 format!("{value:.nr_decimal_digits$}")
118 }
119}
120
121#[cfg(test)]
122mod test_to_string_decimal {
123 use super::Q;
124
125 /// Ensures that [`Q::to_string_decimal`] works for integer values as intended.
126 #[test]
127 fn integer() {
128 let a = Q::from((5, 1));
129 let b = Q::from((256, 8));
130 let c = Q::from((-1, 1));
131
132 let a_0 = a.to_string_decimal(0);
133 let a_1 = a.to_string_decimal(1);
134 let a_2 = a.to_string_decimal(2);
135 let b_0 = b.to_string_decimal(0);
136 let b_1 = b.to_string_decimal(1);
137 let b_5 = b.to_string_decimal(5);
138 let c_0 = c.to_string_decimal(0);
139 let c_1 = c.to_string_decimal(1);
140 let c_2 = c.to_string_decimal(2);
141
142 assert_eq!("5", a_0);
143 assert_eq!("5.0", a_1);
144 assert_eq!("5.00", a_2);
145 assert_eq!("32", b_0);
146 assert_eq!("32.0", b_1);
147 assert_eq!("32.00000", b_5);
148 assert_eq!("-1", c_0);
149 assert_eq!("-1.0", c_1);
150 assert_eq!("-1.00", c_2);
151 }
152
153 /// Ensures that [`Q::to_string_decimal`] works for rational / non-integer values as intended.
154 #[test]
155 fn non_integer() {
156 let a = Q::from((2, 3));
157 let b = Q::from((21, 2));
158 let c = Q::from((-1, 3));
159
160 let a_0 = a.to_string_decimal(0);
161 let a_1 = a.to_string_decimal(1);
162 let a_2 = a.to_string_decimal(2);
163 let b_0 = b.to_string_decimal(0);
164 let b_1 = b.to_string_decimal(1);
165 let b_2 = b.to_string_decimal(2);
166 let c_0 = c.to_string_decimal(0);
167 let c_1 = c.to_string_decimal(1);
168 let c_2 = c.to_string_decimal(2);
169
170 assert_eq!("1", a_0);
171 assert_eq!("0.7", a_1);
172 assert_eq!("0.67", a_2);
173 assert_eq!("10", b_0);
174 assert_eq!("10.5", b_1);
175 assert_eq!("10.50", b_2);
176 assert_eq!("-0", c_0);
177 assert_eq!("-0.3", c_1);
178 assert_eq!("-0.33", c_2);
179 }
180
181 /// Ensures that [`Q::to_string_decimal`] works for large numbers.
182 #[test]
183 fn large_number() {
184 let a = Q::from((i64::MAX, 1));
185
186 let a_0 = a.to_string_decimal(0);
187 let a_1 = a.to_string_decimal(1);
188
189 assert_eq!("9223372036854774784", a_0); // deviation of 1023 from original value
190 assert_eq!("9223372036854774784.0", a_1);
191 }
192}
193
194#[cfg(test)]
195mod test_to_string {
196 use crate::rational::Q;
197 use std::str::FromStr;
198
199 /// Tests whether a large positive rational works in a roundtrip
200 #[test]
201 fn working_large_positive_nom() {
202 let cmp = Q::from(u64::MAX);
203
204 assert_eq!(u64::MAX.to_string(), cmp.to_string());
205 }
206
207 /// Tests whether a large negative rational works in a roundtrip
208 #[test]
209 fn working_large_negative_nom() {
210 let cmp = Q::from_str(&format!("-{}", u64::MAX)).unwrap();
211
212 assert_eq!(format!("-{}", u64::MAX), cmp.to_string());
213 }
214
215 /// Tests whether a large denominator works in a roundtrip
216 #[test]
217 fn working_large_positive_den() {
218 let cmp = Q::from_str(&format!("1/{}", u64::MAX)).unwrap();
219
220 assert_eq!(format!("1/{}", u64::MAX), cmp.to_string());
221 }
222
223 /// Tests whether a large negative denominator works in a roundtrip
224 #[test]
225 fn working_large_negative_den() {
226 let cmp = Q::from_str(&format!("1/-{}", u64::MAX)).unwrap();
227
228 assert_eq!(format!("-1/{}", u64::MAX), cmp.to_string());
229 }
230
231 /// Tests whether a positive rational works in a roundtrip
232 #[test]
233 fn working_positive() {
234 let cmp = Q::from((42, 235));
235
236 assert_eq!("42/235", cmp.to_string());
237 }
238
239 /// Tests whether a negative rational works in a roundtrip
240 #[test]
241 fn working_negative() {
242 let cmp = Q::from((-42, 235));
243
244 assert_eq!("-42/235", cmp.to_string());
245 }
246
247 /// Tests whether a rational that is created using a string, returns a
248 /// string that can be used to create a [`Q`]
249 #[test]
250 fn working_use_result_of_to_string_as_input() {
251 let cmp = Q::from((42, 235));
252
253 let cmp_str_2 = cmp.to_string();
254
255 assert!(Q::from_str(&cmp_str_2).is_ok());
256 }
257
258 /// Ensures that the `Into<String>` trait works properly
259 #[test]
260 fn into_works_properly() {
261 let cmp = "6/7";
262 let rational = Q::from_str(cmp).unwrap();
263
264 let string: String = rational.clone().into();
265 let borrowed_string: String = (&rational).into();
266
267 assert_eq!(cmp, string);
268 assert_eq!(cmp, borrowed_string);
269 }
270}