@@ -3,7 +3,7 @@ use crate::structural::{RationalFacts, RationalStorageClass};
33use num:: bigint:: Sign :: { self , * } ;
44use num:: { BigInt , BigUint , ToPrimitive } ;
55use num:: { One , Zero } ;
6- use serde:: { Deserialize , Serialize } ;
6+ use serde:: { Deserialize , Deserializer , Serialize } ;
77use std:: cmp:: Ordering ;
88use std:: sync:: LazyLock ;
99
@@ -49,13 +49,35 @@ use std::sync::LazyLock;
4949/// assert_eq!(four, Rational::new(4));
5050/// ```
5151
52- #[ derive( Clone , Debug , Serialize , Deserialize ) ]
52+ #[ derive( Clone , Debug , Serialize ) ]
5353pub struct Rational {
5454 sign : Sign ,
5555 numerator : BigUint ,
5656 denominator : BigUint ,
5757}
5858
59+ impl < ' de > Deserialize < ' de > for Rational {
60+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
61+ where
62+ D : Deserializer < ' de > ,
63+ {
64+ #[ derive( Deserialize ) ]
65+ struct RationalWire {
66+ sign : Sign ,
67+ numerator : BigUint ,
68+ denominator : BigUint ,
69+ }
70+
71+ let wire = RationalWire :: deserialize ( deserializer) ?;
72+ if wire. denominator . is_zero ( ) {
73+ return Err ( serde:: de:: Error :: custom (
74+ "Rational denominator must be nonzero" ,
75+ ) ) ;
76+ }
77+ Ok ( Self :: from_fraction_parts ( wire. sign , wire. numerator , wire. denominator ) . reduce ( ) )
78+ }
79+ }
80+
5981static ONE : LazyLock < BigUint > = LazyLock :: new ( BigUint :: one) ;
6082// Small positive constants use their narrow primitive source type; this keeps
6183// construction direct and avoids the older `ToBigUint` conversion shim.
@@ -1613,11 +1635,11 @@ impl std::str::FromStr for Rational {
16131635 if numerator. is_zero ( ) {
16141636 sign = NoSign ;
16151637 }
1616- Ok ( Self {
1617- sign ,
1618- numerator ,
1619- denominator : BigUint :: parse_bytes ( d . as_bytes ( ) , 10 ) . ok_or ( Problem :: BadFraction ) ? ,
1620- } )
1638+ let denominator = BigUint :: parse_bytes ( d . as_bytes ( ) , 10 ) . ok_or ( Problem :: BadFraction ) ? ;
1639+ if denominator . is_zero ( ) {
1640+ return Err ( Problem :: DivideByZero ) ;
1641+ }
1642+ Ok ( Self :: from_fraction_parts ( sign , numerator , denominator ) . reduce ( ) )
16211643 } else if let Some ( ( i, d) ) = s. split_once ( '.' ) {
16221644 let numerator = BigUint :: parse_bytes ( i. as_bytes ( ) , 10 ) . ok_or ( Problem :: BadDecimal ) ?;
16231645 let whole = if numerator. is_zero ( ) {
@@ -1963,6 +1985,27 @@ mod tests {
19631985 assert_eq ! ( answer, expected) ;
19641986 }
19651987
1988+ #[ test]
1989+ fn parse_fraction_rejects_zero_denominator_and_reduces ( ) {
1990+ assert_eq ! ( "1/0" . parse:: <Rational >( ) , Err ( Problem :: DivideByZero ) ) ;
1991+ assert_eq ! ( "0/0" . parse:: <Rational >( ) , Err ( Problem :: DivideByZero ) ) ;
1992+
1993+ let reduced: Rational = "9/18" . parse ( ) . unwrap ( ) ;
1994+ assert_eq ! ( reduced, Rational :: fraction( 1 , 2 ) . unwrap( ) ) ;
1995+ assert_eq ! ( format!( "{reduced}" ) , "1/2" ) ;
1996+ }
1997+
1998+ #[ test]
1999+ fn serde_rejects_invalid_or_uncanonical_rational_state ( ) {
2000+ let bad = r#"{"sign":1,"numerator":[1],"denominator":[]}"# ;
2001+ assert ! ( serde_json:: from_str:: <Rational >( bad) . is_err( ) ) ;
2002+
2003+ let unreduced = r#"{"sign":1,"numerator":[9],"denominator":[18]}"# ;
2004+ let decoded: Rational = serde_json:: from_str ( unreduced) . unwrap ( ) ;
2005+ assert_eq ! ( decoded, Rational :: fraction( 1 , 2 ) . unwrap( ) ) ;
2006+ assert_eq ! ( format!( "{decoded}" ) , "1/2" ) ;
2007+ }
2008+
19662009 #[ test]
19672010 fn square_reduced ( ) {
19682011 let thirty_two = Rational :: new ( 32 ) ;
0 commit comments