Skip to main content

logicaffeine_base/
hyperreal.rs

1//! Hyperreal numbers generated by the program's infinitesimal `dt` — exact, ordered, decidable.
2//!
3//! A program that declares `Let dt be an infinitesimal.` gets a positive number smaller than every
4//! positive standard number. Every hyperreal value built from `dt` with the field operations is a
5//! **rational function of `dt` with real algebraic coefficients**, `N(dt)/D(dt)`, and that is how a
6//! [`Hyperreal`] is held: reduced (`gcd(N, D) = 1`) with `D` *comonic* (its lowest-order nonzero
7//! coefficient is 1), so equal values are structurally equal.
8//!
9//! The order is decided exactly by the transfer principle for real closed fields: a rational
10//! function of `dt` has, for every small enough positive standard `t`, the sign of its
11//! lowest-order coefficient — so `x > 0` exactly when the lowest-order coefficient of `N` is
12//! positive (`D` being comonic). From the same expansion come the standard notions:
13//!
14//! - the **valuation** `v(x) = ord N − ord D` — positive for an infinitesimal, negative for an
15//!   infinite value, zero for an appreciable one;
16//! - the **standard part** — the leading coefficient when `v = 0`, zero when `v > 0`, and none at
17//!   all (the value is not limited) when `v < 0`;
18//! - `x ≈ y` — `x − y` is infinitesimal.
19//!
20//! A value prints as the exact rational function in the infinitesimal's name (`3 + 2·dt`, `1/dt`,
21//! `(1 + dt)/(2 - dt)`), with rational coefficients scaled to integers when that is possible.
22
23use crate::algebraic::Algebraic;
24use crate::ordered_field::PolyOver;
25use crate::numeric::{BigInt, Rational};
26use std::cmp::Ordering;
27use std::fmt;
28use std::hash::{Hash, Hasher};
29
30/// The highest degree one power may reach, so a single operation's work stays bounded.
31pub const MAX_POWER_DEGREE: u64 = 1 << 12;
32
33/// Why a power is refused.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum PowerError {
36    /// Zero to a negative power.
37    ZeroToNegative,
38    /// The power's degree in the infinitesimal would pass [`MAX_POWER_DEGREE`].
39    DegreeTooHigh { degree: u128 },
40}
41
42impl PowerError {
43    /// The refusal, with the infinitesimal written `inf`.
44    pub fn describe(&self, inf: &str) -> String {
45        match self {
46            PowerError::ZeroToNegative => "zero cannot be raised to a negative power".to_string(),
47            PowerError::DegreeTooHigh { degree } => format!(
48                "the power would have degree {degree} in {inf}; one power reaches at most degree 4,096"
49            ),
50        }
51    }
52}
53
54impl fmt::Display for PowerError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(&self.describe("the infinitesimal"))
57    }
58}
59
60/// Refuse the `k`-th power of a value of degree `degree` when it would pass [`MAX_POWER_DEGREE`];
61/// a standard value has no degree to bound.
62pub(crate) fn check_power_degree(degree: usize, k: u64) -> Result<(), PowerError> {
63    let reach = degree as u128 * u128::from(k);
64    if reach > u128::from(MAX_POWER_DEGREE) {
65        return Err(PowerError::DegreeTooHigh { degree: reach });
66    }
67    Ok(())
68}
69
70/// A polynomial in `dt` with real algebraic coefficients, little-endian, no trailing zero.
71type APoly = PolyOver<Algebraic>;
72
73/// A hyperreal number generated by the infinitesimal. See the module documentation.
74#[derive(Clone, Debug)]
75pub struct Hyperreal {
76    num: APoly,
77    den: APoly,
78}
79
80impl Hyperreal {
81    /// Reduce `num/den` and make `den` comonic.
82    fn make(num: APoly, den: APoly) -> Hyperreal {
83        if num.is_zero() {
84            return Hyperreal {
85                num,
86                den: APoly::one(),
87            };
88        }
89        if den.degree() == Some(0) {
90            let inv = den.c[0].recip().expect("the denominator is nonzero");
91            return Hyperreal {
92                num: num.scale(&inv),
93                den: APoly::one(),
94            };
95        }
96        let g = num.gcd(&den);
97        let num = num.div_rem(&g).0;
98        let den = den.div_rem(&g).0;
99        let low = den.coeff(den.order().expect("the denominator is nonzero"));
100        let inv = low.recip().expect("nonzero");
101        Hyperreal {
102            num: num.scale(&inv),
103            den: den.scale(&inv),
104        }
105    }
106
107    /// `num/den` in its one form.
108    pub(crate) fn from_parts(num: APoly, den: APoly) -> Hyperreal {
109        Self::make(num, den)
110    }
111
112    /// The reduced numerator and the comonic denominator.
113    pub(crate) fn parts(&self) -> (&APoly, &APoly) {
114        (&self.num, &self.den)
115    }
116
117    /// The infinitesimal itself.
118    pub fn dt() -> Hyperreal {
119        Hyperreal {
120            num: APoly::from(vec![Algebraic::from_i64(0), Algebraic::from_i64(1)]),
121            den: APoly::one(),
122        }
123    }
124
125    pub fn from_algebraic(a: Algebraic) -> Hyperreal {
126        Hyperreal {
127            num: APoly::constant(a),
128            den: APoly::one(),
129        }
130    }
131
132    pub fn from_rational(r: Rational) -> Hyperreal {
133        Self::from_algebraic(Algebraic::from_rational(r))
134    }
135
136    pub fn from_i64(n: i64) -> Hyperreal {
137        Self::from_algebraic(Algebraic::from_i64(n))
138    }
139
140    /// `Σ cᵢ·(1/dt)^i` for rational `c` (little-endian), written directly as
141    /// `(Σ cᵢ·dt^(d−i)) / dt^d`: with the top coefficient nonzero the numerator has a nonzero
142    /// constant term, so the fraction is already in lowest terms and `dt^d` is comonic — no
143    /// polynomial gcd at any degree.
144    pub fn from_reciprocal_powers(c: &[Rational]) -> Hyperreal {
145        let Some(d) = c.iter().rposition(|x| !x.is_zero()) else {
146            return Self::from_i64(0);
147        };
148        let num = APoly::from((0..=d).map(|j| Algebraic::from_rational(c[d - j].clone())).collect());
149        let mut den = vec![Algebraic::from_i64(0); d + 1];
150        den[d] = Algebraic::from_i64(1);
151        Hyperreal {
152            num,
153            den: APoly::from(den),
154        }
155    }
156
157    pub fn is_zero(&self) -> bool {
158        self.num.is_zero()
159    }
160
161    pub fn add(&self, o: &Hyperreal) -> Hyperreal {
162        if self.den == o.den {
163            return Self::make(self.num.add(&o.num), self.den.clone());
164        }
165        Self::make(
166            self.num.mul(&o.den).add(&o.num.mul(&self.den)),
167            self.den.mul(&o.den),
168        )
169    }
170
171    pub fn neg(&self) -> Hyperreal {
172        Hyperreal {
173            num: self.num.neg(),
174            den: self.den.clone(),
175        }
176    }
177
178    pub fn sub(&self, o: &Hyperreal) -> Hyperreal {
179        self.add(&o.neg())
180    }
181
182    pub fn mul(&self, o: &Hyperreal) -> Hyperreal {
183        Self::make(self.num.mul(&o.num), self.den.mul(&o.den))
184    }
185
186    pub fn recip(&self) -> Option<Hyperreal> {
187        if self.is_zero() {
188            return None;
189        }
190        Some(Self::make(self.den.clone(), self.num.clone()))
191    }
192
193    pub fn div(&self, o: &Hyperreal) -> Option<Hyperreal> {
194        Some(self.mul(&o.recip()?))
195    }
196
197    /// The rational `c` with `self = Σ cᵢ·(1/dt)^i`, when the value is a polynomial in `1/dt`
198    /// over the rationals; `None` for a positive power of `dt`, an irrational coefficient, or a
199    /// denominator that is not a power of `dt`. The inverse of
200    /// [`Hyperreal::from_reciprocal_powers`].
201    pub fn as_reciprocal_powers(&self) -> Option<Vec<Rational>> {
202        if self.num.is_zero() {
203            return Some(Vec::new());
204        }
205        let d = self.den.degree()?;
206        if self.den.order() != Some(d) || self.num.degree()? > d {
207            return None;
208        }
209        (0..=d).map(|i| self.num.coeff(d - i).to_rational()).collect()
210    }
211
212    /// The principal part of the Laurent expansion at `dt = 0` and the limited rest: the
213    /// coefficient of `(1/dt)^j` at index `j − 1` for each `j` up to the pole's order, and
214    /// `self − Σ c_j·(1/dt)^j` exactly. Writing `self = dt^(−k)·N/U` with `U(0) ≠ 0`, the
215    /// coefficients are the first `k` terms of the power series `N/U`.
216    pub fn principal_part(&self) -> (Vec<Algebraic>, Hyperreal) {
217        let k = match self.valuation() {
218            Some(v) if v < 0 => (-v) as usize,
219            _ => return (Vec::new(), self.clone()),
220        };
221        let numerator = &self.num.c[self.num.order().expect("a pole is nonzero")..];
222        let unit = &self.den.c[self.den.order().expect("the denominator is nonzero")..];
223        let unit_inv = unit[0].recip().expect("the unit's constant term is nonzero");
224        let zero = Algebraic::from_i64(0);
225        let mut series: Vec<Algebraic> = Vec::with_capacity(k);
226        for i in 0..k {
227            let mut acc = numerator.get(i).cloned().unwrap_or_else(|| zero.clone());
228            for j in 1..=i {
229                if let Some(u) = unit.get(j) {
230                    acc = acc.sub(&u.mul(&series[i - j]));
231                }
232            }
233            series.push(acc.mul(&unit_inv));
234        }
235        let mut pole = vec![zero; k + 1];
236        pole[k] = Algebraic::from_i64(1);
237        let principal_value = Self::make(APoly::from(series.clone()), APoly::from(pole));
238        let rest = self.sub(&principal_value);
239        let coefficients = (1..=k).map(|j| series[k - j].clone()).collect();
240        (coefficients, rest)
241    }
242
243    /// The coefficients of a polynomial in `dt`, lowest power first, when the value is one.
244    pub fn as_dt_polynomial(&self) -> Option<Vec<Algebraic>> {
245        (self.den == APoly::one()).then(|| self.num.c.clone())
246    }
247
248    /// The coefficient of `dt^k` in the Taylor series of a limited value at `dt = 0`. A limited
249    /// value in lowest terms has `den(0) = 1`, so `s_k = num_k − Σ_{j=1}^{k} den_j·s_{k−j}`.
250    pub fn taylor_coefficient(&self, k: usize) -> Algebraic {
251        debug_assert!(self.is_limited(), "a Taylor series is of a limited value");
252        let mut s: Vec<Algebraic> = Vec::with_capacity(k + 1);
253        for i in 0..=k {
254            let mut acc = self.num.coeff(i);
255            for j in 1..=i {
256                let d = self.den.coeff(j);
257                if !d.is_zero() {
258                    acc = acc.sub(&d.mul(&s[i - j]));
259                }
260            }
261            s.push(acc);
262        }
263        s.pop().expect("k + 1 terms")
264    }
265
266    /// `self = outside^n · inside`, where every polynomial factor of `inside`'s numerator and
267    /// denominator appears fewer than `n` times (Yun's squarefree decomposition takes the `n`-th powers
268    /// out) and `inside`'s constant is taken out only when it is an `n`-th power: with rational
269    /// coefficients the factors left inside are primitive integer polynomials with a positive
270    /// lowest-order coefficient and the constant is the rest, otherwise they are monic and the constant's
271    /// `n`-th root — real, the positive one for even `n` — goes outside. `None` for zero, and for an even
272    /// `n` of a negative value.
273    pub fn root_split(&self, n: u32) -> Option<(Hyperreal, Hyperreal)> {
274        if self.is_zero() || (n % 2 == 0 && self.signum() < 0) {
275            return None;
276        }
277        let (num_lc, num_parts) = self.num.squarefree_parts();
278        let (den_lc, den_parts) = self.den.squarefree_parts();
279        let power = |parts: &[(APoly, usize)], take: &dyn Fn(usize) -> usize| {
280            parts
281                .iter()
282                .fold(APoly::one(), |acc, (a, i)| (0..take(*i)).fold(acc, |acc, _| acc.mul(a)))
283        };
284        let n = n as usize;
285        let out_num = power(&num_parts, &|i| i / n);
286        let out_den = power(&den_parts, &|i| i / n);
287        let mut in_num = power(&num_parts, &|i| i % n);
288        let mut in_den = power(&den_parts, &|i| i % n);
289        let mut constant = num_lc.div(&den_lc)?;
290        let rational = in_num.c.iter().chain(&in_den.c).all(|a| a.to_rational().is_some())
291            && constant.to_rational().is_some();
292        if rational {
293            let (sn, sd) = (primitive_scale(&in_num), primitive_scale(&in_den));
294            in_num = in_num.scale(&sn);
295            in_den = in_den.scale(&sd);
296            constant = constant.div(&sn)?.mul(&sd);
297        } else {
298            let ln = in_num.coeff(in_num.order().unwrap_or(0));
299            let ld = in_den.coeff(in_den.order().unwrap_or(0));
300            in_num = in_num.scale(&ln.recip()?);
301            in_den = in_den.scale(&ld.recip()?);
302            constant = constant.mul(&ln).div(&ld)?;
303        }
304        let root = constant.nth_root(n as u32).filter(|r| !rational || r.to_rational().is_some());
305        let (outside_constant, inside_constant) = match root {
306            Some(r) => (r, Algebraic::from_i64(1)),
307            None => (Algebraic::from_i64(1), constant),
308        };
309        let outside = Self::make(out_num.scale(&outside_constant), out_den);
310        let inside = Self::make(in_num.scale(&inside_constant), in_den);
311        Some((outside, inside))
312    }
313
314    /// Whether the numerator is a constant: the value is a constant over a polynomial.
315    pub fn numerator_is_constant(&self) -> bool {
316        self.num.degree().unwrap_or(0) == 0
317    }
318
319    /// The degrees of the reduced numerator and denominator; `(0, 0)` for zero.
320    pub fn degrees(&self) -> (usize, usize) {
321        (self.num.degree().unwrap_or(0), self.den.degree().unwrap_or(0))
322    }
323
324    /// `max(deg num, deg den)`: how far the value reaches in `dt`.
325    pub fn degree(&self) -> usize {
326        self.num.degree().unwrap_or(0).max(self.den.degree().unwrap_or(0))
327    }
328
329    /// `self^k`, refused for zero to a negative power or a degree past [`MAX_POWER_DEGREE`].
330    pub fn pow(&self, k: i32) -> Result<Hyperreal, PowerError> {
331        check_power_degree(self.degree(), u64::from(k.unsigned_abs()))?;
332        let mut base = if k < 0 {
333            self.recip().ok_or(PowerError::ZeroToNegative)?
334        } else {
335            self.clone()
336        };
337        let mut acc = Self::from_i64(1);
338        let mut e = k.unsigned_abs();
339        while e > 0 {
340            if e & 1 == 1 {
341                acc = acc.mul(&base);
342            }
343            e >>= 1;
344            if e > 0 {
345                base = base.mul(&base);
346            }
347        }
348        Ok(acc)
349    }
350
351    /// `−1`, `0` or `1`: the sign of the lowest-order coefficient of the numerator.
352    pub fn signum(&self) -> i32 {
353        match self.num.order() {
354            None => 0,
355            Some(i) => self.num.c[i].signum(),
356        }
357    }
358
359    /// The standard value, when the value has no infinitesimal part at all.
360    pub fn as_standard(&self) -> Option<Algebraic> {
361        if self.den != APoly::one() || self.num.c.len() > 1 {
362            return None;
363        }
364        Some(self.num.coeff(0))
365    }
366
367    /// `ord N − ord D`; `None` for zero.
368    pub fn valuation(&self) -> Option<i64> {
369        let n = self.num.order()? as i64;
370        let d = self.den.order().expect("the denominator is nonzero") as i64;
371        Some(n - d)
372    }
373
374    /// The coefficient of the leading power of `dt`.
375    pub fn leading_coefficient(&self) -> Option<Algebraic> {
376        Some(self.num.c[self.num.order()?].clone())
377    }
378
379    pub fn is_infinitesimal(&self) -> bool {
380        self.valuation().is_none_or(|v| v > 0)
381    }
382
383    pub fn is_limited(&self) -> bool {
384        self.valuation().is_none_or(|v| v >= 0)
385    }
386
387    /// The standard real infinitely close to a limited value; `None` for an infinite one.
388    pub fn standard_part(&self) -> Option<Algebraic> {
389        match self.valuation() {
390            None => Some(Algebraic::from_i64(0)),
391            Some(v) if v > 0 => Some(Algebraic::from_i64(0)),
392            Some(0) => self.leading_coefficient(),
393            Some(_) => None,
394        }
395    }
396
397    pub fn infinitely_close(&self, o: &Hyperreal) -> bool {
398        self.sub(o).is_infinitesimal()
399    }
400
401    /// The value at the standard point `dt = t`; `None` at a pole.
402    pub fn eval_at(&self, t: &Rational) -> Option<Algebraic> {
403        let t = Algebraic::from_rational(t.clone());
404        self.num.eval(&t).div(&self.den.eval(&t))
405    }
406
407    /// `c·dt^k`, for any integer `k`.
408    pub fn monomial(c: Algebraic, k: i64) -> Hyperreal {
409        let zero = || Algebraic::from_i64(0);
410        let power = |n: usize| {
411            let mut p = vec![zero(); n + 1];
412            p[n] = Algebraic::from_i64(1);
413            APoly::from(p)
414        };
415        if c.is_zero() {
416            return Self::from_i64(0);
417        }
418        if k >= 0 {
419            Hyperreal {
420                num: power(k as usize).scale(&c),
421                den: APoly::one(),
422            }
423        } else {
424            Hyperreal {
425                num: APoly::constant(c),
426                den: power(k.unsigned_abs() as usize),
427            }
428        }
429    }
430
431    /// The same function of `dt^m`: `dt` replaced by `dt^m`, which keeps a reduced fraction reduced
432    /// and a comonic denominator comonic.
433    pub fn ramify(&self, m: u32) -> Hyperreal {
434        let spread = |p: &APoly| {
435            let mut c = vec![Algebraic::from_i64(0); p.c.len().saturating_sub(1) * m as usize + 1];
436            for (i, a) in p.c.iter().enumerate() {
437                c[i * m as usize] = a.clone();
438            }
439            APoly::from(c)
440        };
441        Hyperreal {
442            num: spread(&self.num),
443            den: spread(&self.den),
444        }
445    }
446
447    /// The function `r` with `self = r(dt^m)`, when every power of `dt` the value holds is a
448    /// multiple of `m`; `None` otherwise.
449    pub fn unramify(&self, m: u32) -> Option<Hyperreal> {
450        let gather = |p: &APoly| -> Option<APoly> {
451            let mut c = Vec::new();
452            for (i, a) in p.c.iter().enumerate() {
453                if i % m as usize == 0 {
454                    c.push(a.clone());
455                } else if !a.is_zero() {
456                    return None;
457                }
458            }
459            Some(APoly::from(c))
460        };
461        Some(Hyperreal {
462            num: gather(&self.num)?,
463            den: gather(&self.den)?,
464        })
465    }
466
467    /// The coefficient of `dt^k` in the Laurent expansion at `dt = 0`.
468    pub fn laurent_coefficient(&self, k: i64) -> Algebraic {
469        let (principal, rest) = self.principal_part();
470        if k < 0 {
471            let j = k.unsigned_abs() as usize;
472            return principal.get(j - 1).cloned().unwrap_or_else(|| Algebraic::from_i64(0));
473        }
474        rest.taylor_coefficient(k as usize)
475    }
476
477    /// The rational function of numerator degree at most `l` and denominator degree at most `m`,
478    /// denominator nonzero at 0, whose expansion at 0 begins with `series` (its first `l + m + 1`
479    /// coefficients), by the extended Euclidean algorithm on `dt^(l+m+1)` and the truncated series;
480    /// `None` when no such function exists.
481    pub fn pade(series: &[Algebraic], l: usize, m: usize) -> Option<Hyperreal> {
482        let n = l + m + 1;
483        let mut top = vec![Algebraic::from_i64(0); n + 1];
484        top[n] = Algebraic::from_i64(1);
485        let truncated: Vec<Algebraic> = (0..n)
486            .map(|i| series.get(i).cloned().unwrap_or_else(|| Algebraic::from_i64(0)))
487            .collect();
488        let (mut r0, mut r1) = (APoly::from(top), APoly::from(truncated));
489        let (mut t0, mut t1) = (APoly::zero(), APoly::one());
490        while r1.degree().is_some_and(|d| d > l) {
491            let (q, r) = r0.div_rem(&r1);
492            let t = t0.add(&q.mul(&t1).neg());
493            r0 = std::mem::replace(&mut r1, r);
494            t0 = std::mem::replace(&mut t1, t);
495        }
496        if t1.degree().unwrap_or(0) > m || t1.coeff(0).is_zero() {
497            return None;
498        }
499        Some(Self::make(r1, t1))
500    }
501
502    /// The exact rational function with the infinitesimal written as `name`.
503    pub fn render(&self, name: &str) -> String {
504        self.render_powers(&|k| match k {
505            1 => name.to_string(),
506            k => format!("{name}^{k}"),
507        })
508    }
509
510    /// The exact rational function with the infinitesimal's `k`-th power written `power(k)`, for
511    /// `k ≥ 1`.
512    pub fn render_powers(&self, power: &dyn Fn(usize) -> String) -> String {
513        if self.den == APoly::one() {
514            return poly_text(&self.num, power);
515        }
516        let (num, den) = self.display_parts();
517        let terms = |p: &APoly| p.c.iter().filter(|a| !a.is_zero()).count();
518        let numerator = poly_text(&num, power);
519        let numerator = if terms(&num) > 1 { format!("({numerator})") } else { numerator };
520        let denominator = poly_text(&den, power);
521        let denominator = if terms(&den) > 1 || denominator.contains('·') {
522            format!("({denominator})")
523        } else {
524            denominator
525        };
526        format!("{numerator}/{denominator}")
527    }
528
529    /// The numerator and denominator the value prints as: scaled to coprime integers, the
530    /// denominator's lowest coefficient positive, when every coefficient is rational; the comonic form
531    /// otherwise.
532    pub(crate) fn display_fraction(&self) -> (APoly, APoly) {
533        self.display_parts()
534    }
535
536    /// Numerator and denominator scaled to coprime integers (denominator's lowest coefficient
537    /// positive) when every coefficient is rational; the comonic form otherwise.
538    fn display_parts(&self) -> (APoly, APoly) {
539        let all: Vec<&Algebraic> = self.num.c.iter().chain(self.den.c.iter()).collect();
540        let rationals: Option<Vec<Rational>> = all.iter().map(|a| a.to_rational()).collect();
541        let Some(rs) = rationals else {
542            return (self.num.clone(), self.den.clone());
543        };
544        let lcm = rs.iter().fold(BigInt::from_i64(1), |l, r| {
545            let d = r.denominator();
546            let g = l.gcd(d);
547            l.div_rem(&g).expect("nonzero").0.mul(d)
548        });
549        let ints: Vec<BigInt> = rs
550            .iter()
551            .map(|r| {
552                r.numerator()
553                    .mul(&lcm.div_rem(r.denominator()).expect("nonzero").0)
554            })
555            .collect();
556        let g = ints.iter().fold(BigInt::zero(), |g, x| g.gcd(x));
557        let k = Algebraic::from_rational(
558            Rational::new(lcm, if g.is_zero() { BigInt::from_i64(1) } else { g }).expect("nonzero"),
559        );
560        (self.num.scale(&k), self.den.scale(&k))
561    }
562}
563
564/// The rational `k` that makes `p·k` a primitive integer polynomial with a positive lowest-order
565/// coefficient, for `p` with rational coefficients.
566fn primitive_scale(p: &APoly) -> Algebraic {
567    let rs: Vec<Rational> = p.c.iter().map(|a| a.to_rational().expect("rational coefficients")).collect();
568    let lcm = rs.iter().fold(BigInt::from_i64(1), |l, r| {
569        let d = r.denominator();
570        let g = l.gcd(d);
571        l.div_rem(&g).expect("nonzero").0.mul(d)
572    });
573    let ints: Vec<BigInt> = rs
574        .iter()
575        .map(|r| r.numerator().mul(&lcm.div_rem(r.denominator()).expect("nonzero").0))
576        .collect();
577    let g = ints.iter().fold(BigInt::zero(), |g, x| g.gcd(x));
578    let low_negative = ints.iter().find(|x| !x.is_zero()).is_some_and(|x| x.is_negative());
579    let g = if low_negative { g.negated() } else { g };
580    Algebraic::from_rational(Rational::new(lcm, if g.is_zero() { BigInt::from_i64(1) } else { g }).expect("nonzero"))
581}
582
583/// `c₀ + c₁·dt + c₂·dt^2 + …` in ascending powers, signs between the terms, the `k`-th power of
584/// the infinitesimal written `power(k)`.
585fn poly_text(p: &APoly, power: &dyn Fn(usize) -> String) -> String {
586    if p.is_zero() {
587        return "0".to_string();
588    }
589    let mut out = String::new();
590    let mut first = true;
591    for (i, a) in p.c.iter().enumerate() {
592        if a.is_zero() {
593            continue;
594        }
595        let negative = a.signum() < 0;
596        let magnitude = if negative { a.neg() } else { a.clone() };
597        let power = if i == 0 { String::new() } else { power(i) };
598        let body = if i == 0 {
599            magnitude.to_string()
600        } else if magnitude == Algebraic::from_i64(1) {
601            power
602        } else {
603            let m = magnitude.to_string();
604            if m.contains(' ') || m.contains('/') {
605                format!("({m})·{power}")
606            } else {
607                format!("{m}·{power}")
608            }
609        };
610        match (first, negative) {
611            (true, true) => out.push_str(&format!("-{body}")),
612            (true, false) => out.push_str(&body),
613            (false, true) => out.push_str(&format!(" - {body}")),
614            (false, false) => out.push_str(&format!(" + {body}")),
615        }
616        first = false;
617    }
618    out
619}
620
621impl PartialEq for Hyperreal {
622    fn eq(&self, o: &Self) -> bool {
623        self.num == o.num && self.den == o.den
624    }
625}
626
627impl Eq for Hyperreal {}
628
629impl Hash for Hyperreal {
630    fn hash<H: Hasher>(&self, state: &mut H) {
631        self.num.hash(state);
632        self.den.hash(state);
633    }
634}
635
636impl Ord for Hyperreal {
637    fn cmp(&self, o: &Self) -> Ordering {
638        match self.sub(o).signum() {
639            -1 => Ordering::Less,
640            0 => Ordering::Equal,
641            _ => Ordering::Greater,
642        }
643    }
644}
645
646impl PartialOrd for Hyperreal {
647    fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
648        Some(self.cmp(o))
649    }
650}
651
652impl fmt::Display for Hyperreal {
653    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654        write!(f, "{}", self.render("dt"))
655    }
656}
657
658impl crate::exact_codec::ExactCodec for Hyperreal {
659    /// The reduced numerator's coefficients, then the comonic denominator's, lowest power first.
660    fn write(&self, out: &mut Vec<u8>) {
661        crate::exact_codec::write_list(&self.num.c, out);
662        crate::exact_codec::write_list(&self.den.c, out);
663    }
664
665    fn read(buf: &[u8], pos: &mut usize) -> Option<Self> {
666        let num = APoly::from(crate::exact_codec::read_list(buf, pos)?);
667        let den = APoly::from(crate::exact_codec::read_list(buf, pos)?);
668        if den.is_zero() {
669            return None;
670        }
671        Some(Hyperreal::make(num, den))
672    }
673}
674
675#[cfg(test)]
676#[path = "../../logicaffeine_tests/unit/logicaffeine_base/hyperreal/tests.rs"]
677mod tests;