@@ -3,13 +3,12 @@ use crate::avm1::error::Error;
33use crate :: avm1:: object:: value_object:: ValueObject ;
44use crate :: avm1:: { Object , TObject } ;
55use crate :: ecma_conversions:: {
6- f64_to_string , f64_to_wrapping_i16, f64_to_wrapping_i32, f64_to_wrapping_u16,
7- f64_to_wrapping_u32 , f64_to_wrapping_u8,
6+ f64_to_wrapping_i16, f64_to_wrapping_i32, f64_to_wrapping_u16, f64_to_wrapping_u32 ,
7+ f64_to_wrapping_u8,
88} ;
99use crate :: string:: { AvmString , Integer , WStr } ;
1010use gc_arena:: Collect ;
11- use std:: borrow:: Cow ;
12- use std:: num:: Wrapping ;
11+ use std:: { borrow:: Cow , io:: Write , num:: Wrapping } ;
1312
1413#[ derive( Debug , Clone , Copy , Collect ) ]
1514#[ collect( no_drop) ]
@@ -496,6 +495,218 @@ impl<'gc> Value<'gc> {
496495 }
497496}
498497
498+ /// Converts an `f64` to a String with (hopefully) the same output as Flash AVM1.
499+ /// 15 digits are displayed (not including leading 0s in a decimal <1).
500+ /// Exponential notation is used for numbers <= 1e-5 and >= 1e15.
501+ /// Rounding done with ties rounded away from zero.
502+ /// NAN returns `"NaN"`, and infinity returns `"Infinity"`.
503+ #[ allow( clippy:: approx_constant) ]
504+ fn f64_to_string ( mut n : f64 ) -> Cow < ' static , str > {
505+ if n. is_nan ( ) {
506+ Cow :: Borrowed ( "NaN" )
507+ } else if n == f64:: INFINITY {
508+ Cow :: Borrowed ( "Infinity" )
509+ } else if n == f64:: NEG_INFINITY {
510+ Cow :: Borrowed ( "-Infinity" )
511+ } else if n == 0.0 {
512+ Cow :: Borrowed ( "0" )
513+ } else if n >= -2147483648.0 && n <= 2147483647.0 && n. fract ( ) == 0.0 {
514+ // Fast path for integers.
515+ ( n as i32 ) . to_string ( ) . into ( )
516+ } else {
517+ // AVM1 f64 -> String (also trying to reproduce bugs).
518+ // Flash Player's AVM1 does this in a straightforward way, shifting the float into the
519+ // range of [0.0, 10.0), repeatedly multiplying by 10 to extract digits, and then finally
520+ // rounding the result. However, the rounding is buggy, when carrying 9.999 -> 10.
521+ // For example, -9999999999999999.0 results in "-e+16".
522+ let mut buf: Vec < u8 > = Vec :: with_capacity ( 25 ) ;
523+ let is_negative = if n < 0.0 {
524+ n = -n;
525+ buf. push ( b'-' ) ;
526+ true
527+ } else {
528+ false
529+ } ;
530+
531+ // Extract base-2 exponent from double-precision float (11 bits, biased by 1023).
532+ const MANTISSA_BITS : u64 = 52 ;
533+ const EXPONENT_MASK : u64 = 0x7ff ;
534+ const EXPONENT_BIAS : i32 = 1023 ;
535+ let mut exp_base2: i32 =
536+ ( ( n. to_bits ( ) >> MANTISSA_BITS ) & EXPONENT_MASK ) as i32 - EXPONENT_BIAS ;
537+
538+ if exp_base2 == -EXPONENT_BIAS {
539+ // Subnormal float; scale back into normal range and retry getting the exponent.
540+ const NORMAL_SCALE : f64 = 1.801439850948198e16 ; // 2^54
541+ let n = n * NORMAL_SCALE ;
542+ exp_base2 =
543+ ( ( n. to_bits ( ) >> MANTISSA_BITS ) & EXPONENT_MASK ) as i32 - EXPONENT_BIAS - 54 ;
544+ }
545+
546+ // Convert to base-10 exponent.
547+ const LOG10_2 : f64 = 0.301029995663981 ; // log_10(2) value (less precise than Rust's f64::LOG10_2).
548+ let mut exp = f64:: round ( f64:: from ( exp_base2) * LOG10_2 ) as i32 ;
549+
550+ // Calculate `value * 10^exp` through repeated multiplication or division.
551+ fn decimal_shift ( mut value : f64 , mut exp : i32 ) -> f64 {
552+ let mut base: f64 = 10.0 ;
553+ // The multiply and division branches are intentionally separate to match Flash's behavior.
554+ if exp > 0 {
555+ while exp > 0 {
556+ if ( exp & 1 ) != 0 {
557+ value *= base;
558+ }
559+ exp >>= 1 ;
560+ base *= base;
561+ }
562+ } else {
563+ exp = -exp;
564+ while exp > 0 {
565+ if ( exp & 1 ) != 0 {
566+ value /= base;
567+ }
568+ exp >>= 1 ;
569+ base *= base;
570+ }
571+ } ;
572+ value
573+ }
574+
575+ // Shift the decimal value so that it's in the range of [0.0, 10.0).
576+ let mut mantissa: f64 = decimal_shift ( n, -exp) ;
577+
578+ // The exponent calculation can be off by 1; try the next exponent if so.
579+ if mantissa as i32 == 0 {
580+ exp -= 1 ;
581+ mantissa = decimal_shift ( n, -exp) ;
582+ }
583+ if mantissa as i32 >= 10 {
584+ exp += 1 ;
585+ mantissa = decimal_shift ( n, -exp) ;
586+ }
587+
588+ // Generates the next digit character.
589+ let mut digit = || {
590+ let digit: i32 = mantissa as i32 ;
591+ debug_assert ! ( digit >= 0 && digit < 10 ) ;
592+ mantissa -= f64:: from ( digit) ;
593+ mantissa *= 10.0 ;
594+ b'0' + digit as u8
595+ } ;
596+
597+ const MAX_DECIMAL_PLACES : i32 = 15 ;
598+ match exp {
599+ 15 .. => {
600+ // 1.2345e+15
601+ // This case fails to push an extra 0 to handle the rounding 9.9999 -> 10, which
602+ // causes the -9999999999999999.0 -> "-e+16" bug later.
603+ buf. push ( digit ( ) ) ;
604+ buf. push ( b'.' ) ;
605+ for _ in 0 ..MAX_DECIMAL_PLACES - 1 {
606+ buf. push ( digit ( ) ) ;
607+ }
608+ }
609+ 0 ..=14 => {
610+ // 12345.678901234
611+ buf. push ( b'0' ) ;
612+ for _ in 0 ..=exp {
613+ buf. push ( digit ( ) ) ;
614+ }
615+ buf. push ( b'.' ) ;
616+ for _ in 0 ..MAX_DECIMAL_PLACES - exp - 1 {
617+ buf. push ( digit ( ) ) ;
618+ }
619+ exp = 0 ;
620+ }
621+ -5 ..=-1 => {
622+ // 0.0012345678901234
623+ buf. extend_from_slice ( b"00." ) ;
624+ buf. resize ( buf. len ( ) + ( -exp) as usize - 1 , b'0' ) ;
625+ for _ in 0 ..MAX_DECIMAL_PLACES {
626+ buf. push ( digit ( ) ) ;
627+ }
628+ exp = 0 ;
629+ }
630+ _ => {
631+ // 1.345e-15
632+ buf. push ( b'0' ) ;
633+ let n = digit ( ) ;
634+ if n != 0 {
635+ buf. push ( n) ;
636+ }
637+ buf. push ( b'.' ) ;
638+ for _ in 0 ..MAX_DECIMAL_PLACES - 1 {
639+ buf. push ( digit ( ) ) ;
640+ }
641+ }
642+ } ;
643+
644+ // Rounding: Peek at the next generated digit and round accordingly.
645+ // Ties round away from zero.
646+ if digit ( ) >= b'5' {
647+ // Add 1 to the right-most digit, carrying if we hit a 9.
648+ for c in buf. iter_mut ( ) . rev ( ) {
649+ if * c == b'9' {
650+ * c = b'0' ;
651+ } else if * c >= b'0' {
652+ * c += 1 ;
653+ break ;
654+ }
655+ }
656+ }
657+
658+ // Trim any trailing zeros and decimal point.
659+ while buf. last ( ) == Some ( & b'0' ) {
660+ buf. pop ( ) ;
661+ }
662+ if buf. last ( ) == Some ( & b'.' ) {
663+ buf. pop ( ) ;
664+ }
665+
666+ let mut start = 0 ;
667+ if exp != 0 {
668+ // Write exponent (e+###).
669+
670+ // Lots of band-aids here to attempt to clean up the rounding above.
671+ // Negative values are not correctly handled in the Flash Player, causing several bugs.
672+ // PLAYER-SPECIFIC: I think these checks were added in Flash Player 6.
673+ // Trim leading zeros.
674+ let pos = buf. iter ( ) . position ( |& c| c != b'0' ) . unwrap_or ( buf. len ( ) ) ;
675+ if pos != 0 {
676+ buf. copy_within ( pos.., 0 ) ;
677+ buf. truncate ( buf. len ( ) - pos) ;
678+ }
679+ if buf. is_empty ( ) {
680+ // Fix up 9.99999 being rounded to 0.00000 when there is no space for the carried 1.
681+ // If we have no digits, the value was all 0s that were trimmed, so round to 1.
682+ buf. push ( b'1' ) ;
683+ exp += 1 ;
684+ } else {
685+ // Fix up 100e15 to 1e17.
686+ let pos = buf. iter ( ) . rposition ( |& c| c != b'0' ) . unwrap_or_default ( ) ;
687+ if pos == 0 {
688+ exp += buf. len ( ) as i32 - 1 ;
689+ buf. truncate ( 1 ) ;
690+ }
691+ }
692+ let _ = write ! ( & mut buf, "e{:+}" , exp) ;
693+ }
694+
695+ // One final band-aid to eliminate any leading zeros.
696+ let i = if is_negative { 1 } else { 0 } ;
697+ if buf. get ( i) == Some ( & b'0' ) && buf. get ( i + 1 ) != Some ( & b'.' ) {
698+ if i > 0 {
699+ buf[ i] = buf[ i - 1 ] ;
700+ }
701+ start = 1 ;
702+ }
703+
704+ // SAFETY: Buffer is guaranteed to only contain ASCII digits.
705+ let s = unsafe { std:: str:: from_utf8_unchecked ( & buf[ start..] ) } ;
706+ s. to_string ( ) . into ( )
707+ }
708+ }
709+
499710#[ cfg( test) ]
500711#[ allow( clippy:: unreadable_literal) ] // Large numeric literals in tests
501712mod test {
@@ -768,5 +979,25 @@ mod test {
768979 assert_eq ! ( f64_to_string( -1e-5 ) , "-0.00001" ) ;
769980 assert_eq ! ( f64_to_string( 0.999e-5 ) , "9.99e-6" ) ;
770981 assert_eq ! ( f64_to_string( -0.999e-5 ) , "-9.99e-6" ) ;
982+ assert_eq ! ( f64_to_string( 0.19999999999999996 ) , "0.2" ) ;
983+ assert_eq ! ( f64_to_string( -0.19999999999999996 ) , "-0.2" ) ;
984+ assert_eq ! ( f64_to_string( 100000.12345678912 ) , "100000.123456789" ) ;
985+ assert_eq ! ( f64_to_string( -100000.12345678912 ) , "-100000.123456789" ) ;
986+ assert_eq ! ( f64_to_string( 0.8000000000000005 ) , "0.800000000000001" ) ;
987+ assert_eq ! ( f64_to_string( -0.8000000000000005 ) , "-0.800000000000001" ) ;
988+ assert_eq ! ( f64_to_string( 0.8300000000000005 ) , "0.83" ) ;
989+ assert_eq ! ( f64_to_string( 1e-320 ) , "9.99988867182684e-321" ) ;
990+ assert_eq ! ( f64_to_string( f64 :: MIN ) , "-1.79769313486231e+308" ) ;
991+ assert_eq ! ( f64_to_string( f64 :: MIN_POSITIVE ) , "2.2250738585072e-308" ) ;
992+ assert_eq ! ( f64_to_string( f64 :: MAX ) , "1.79769313486231e+308" ) ;
993+ assert_eq ! ( f64_to_string( 5e-324 ) , "4.94065645841247e-324" ) ;
994+ assert_eq ! ( f64_to_string( 9.999999999999999 ) , "10" ) ;
995+ assert_eq ! ( f64_to_string( -9.999999999999999 ) , "-10" ) ;
996+ assert_eq ! ( f64_to_string( 9999999999999996.0 ) , "1e+16" ) ;
997+ assert_eq ! ( f64_to_string( -9999999999999996.0 ) , "-e+16" ) ; // wat
998+ assert_eq ! ( f64_to_string( 0.000009999999999999996 ) , "1e-5" ) ;
999+ assert_eq ! ( f64_to_string( -0.000009999999999999996 ) , "-10e-6" ) ;
1000+ assert_eq ! ( f64_to_string( 0.00009999999999999996 ) , "0.0001" ) ;
1001+ assert_eq ! ( f64_to_string( -0.00009999999999999996 ) , "-0.0001" ) ;
7711002 }
7721003}
0 commit comments