@@ -101,8 +101,6 @@ pub struct ScientificNotation<'a> {
101101 int : & ' a str ,
102102 frac : Cow < ' a , str > ,
103103 exp : isize ,
104- scientific : bool ,
105- precision : usize ,
106104}
107105
108106impl PartialEq for ScientificNotation < ' _ > {
@@ -168,60 +166,39 @@ impl<'a> RawNum<'a> {
168166 }
169167
170168 fn normalize ( & mut self ) -> ScientificNotation < ' a > {
171- let scientific = self . exp != 0 ;
172- let precision = self . frac . len ( ) ;
173- if self . int . starts_with ( '0' ) {
169+ if self . int == "0" && !self . frac . is_empty ( ) {
174170 let frac_zeros = self . frac . chars ( ) . take_while ( |& ch| ch == '0' ) . count ( ) ;
175171 #[ expect( clippy:: cast_possible_wrap) ]
176172 let exp = self . exp - 1 - frac_zeros as isize ;
177173 self . frac = & self . frac [ frac_zeros..] ;
178174
179175 match self . frac . len ( ) {
180- 0 => ScientificNotation {
181- int : "0" ,
182- frac : Cow :: Borrowed ( "" ) ,
183- exp,
184- scientific,
185- precision,
186- } ,
187- 1 => ScientificNotation {
188- int : & self . frac [ ..1 ] ,
189- frac : Cow :: Borrowed ( "" ) ,
190- exp,
191- scientific,
192- precision,
193- } ,
176+ 0 => ScientificNotation { int : "0" , frac : Cow :: Borrowed ( "" ) , exp } ,
177+ 1 => ScientificNotation { int : & self . frac [ ..1 ] , frac : Cow :: Borrowed ( "" ) , exp } ,
194178 _ => ScientificNotation {
195179 int : & self . frac [ ..1 ] ,
196180 frac : Cow :: Borrowed ( & self . frac [ 1 ..] ) ,
197181 exp,
198- scientific,
199- precision,
200182 } ,
201183 }
202184 } else {
203185 #[ expect( clippy:: cast_possible_wrap) ]
204186 let exp = self . exp + self . int . len ( ) as isize - 1 ;
205187 if self . int . len ( ) == 1 {
206- ScientificNotation {
207- int : self . int ,
208- frac : Cow :: Borrowed ( self . frac ) ,
209- exp,
210- scientific,
211- precision,
212- }
188+ ScientificNotation { int : self . int , frac : Cow :: Borrowed ( self . frac ) , exp }
213189 } else {
214190 let frac = if self . frac . is_empty ( ) {
215- Cow :: Borrowed ( & self . int [ 1 ..] )
191+ let int_trimmed = self . int . trim_end_matches ( '0' ) ;
192+ if int_trimmed. len ( ) == 1 {
193+ Cow :: Borrowed ( "" )
194+ } else {
195+ Cow :: Borrowed ( & int_trimmed[ 1 ..] )
196+ }
216197 } else {
217- Cow :: Owned (
218- format ! ( "{}{}" , & self . int[ 1 ..] , self . frac)
219- . trim_end_matches ( '0' )
220- . to_string ( ) ,
221- )
198+ Cow :: Owned ( format ! ( "{}{}" , & self . int[ 1 ..] , self . frac) )
222199 } ;
223200
224- ScientificNotation { int : & self . int [ ..1 ] , frac, exp, scientific , precision }
201+ ScientificNotation { int : & self . int [ ..1 ] , frac, exp }
225202 }
226203 }
227204 }
@@ -250,14 +227,14 @@ impl NoLossOfPrecision {
250227 return true ;
251228 } ;
252229
253- if raw. frac . len ( ) >= 100 {
230+ let total_significant_digits = raw. int . len ( ) + raw. frac . len ( ) ;
231+
232+ if total_significant_digits > 100 {
254233 return true ;
255234 }
256- let stored = match ( raw. scientific , raw. precision ) {
257- ( true , _) => format ! ( "{:.1$e}" , node. value, raw. frac. len( ) ) ,
258- ( false , 0 ) => node. value . to_string ( ) ,
259- ( false , precision) => format ! ( "{:.1$}" , node. value, precision) ,
260- } ;
235+
236+ let stored = to_precision ( node. value , total_significant_digits) ;
237+
261238 let Some ( stored) = Self :: normalize ( & stored) else {
262239 return true ;
263240 } ;
@@ -277,6 +254,188 @@ impl NoLossOfPrecision {
277254 }
278255}
279256
257+ /// `flt_str_to_exp` - used in `to_precision`
258+ ///
259+ /// This function traverses a string representing a number,
260+ /// returning the floored log10 of this number.
261+ #[ expect( clippy:: cast_possible_truncation) ]
262+ #[ expect( clippy:: cast_possible_wrap) ]
263+ fn flt_str_to_exp ( flt : & str ) -> i32 {
264+ let mut non_zero_encountered = false ;
265+ let mut dot_encountered = false ;
266+ for ( i, c) in flt. char_indices ( ) {
267+ if c == '.' {
268+ if non_zero_encountered {
269+ return ( i as i32 ) - 1 ;
270+ }
271+ dot_encountered = true ;
272+ } else if c != '0' {
273+ if dot_encountered {
274+ return 1 - ( i as i32 ) ;
275+ }
276+ non_zero_encountered = true ;
277+ }
278+ }
279+ ( flt. len ( ) as i32 ) - 1
280+ }
281+
282+ /// `round_to_precision` - used in `to_precision`
283+ ///
284+ /// This procedure has two roles:
285+ /// - If there are enough or more than enough digits in the
286+ /// string to show the required precision, the number
287+ /// represented by these digits is rounded using string
288+ /// manipulation.
289+ /// - Else, zeroes are appended to the string.
290+ /// - Additionally, sometimes the exponent was wrongly computed and
291+ /// while up-rounding we find that we need an extra digit. When this
292+ /// happens, we return true so that the calling context can adjust
293+ /// the exponent. The string is kept at an exact length of `precision`.
294+ ///
295+ /// When this procedure returns, `digits` is exactly `precision` long.
296+ fn round_to_precision ( digits : & mut String , precision : usize ) -> bool {
297+ if digits. len ( ) > precision {
298+ let to_round = digits. split_off ( precision) ;
299+ let mut digit =
300+ digits. pop ( ) . expect ( "already checked that length is bigger than precision" ) as u8 ;
301+ if let Some ( first) = to_round. chars ( ) . next ( ) {
302+ if first > '4' {
303+ digit += 1 ;
304+ }
305+ }
306+
307+ if digit as char == ':' {
308+ // ':' is '9' + 1
309+ // need to propagate the increment backward
310+ let mut replacement = String :: from ( "0" ) ;
311+ let mut propagated = false ;
312+ for c in digits. chars ( ) . rev ( ) {
313+ let d = match ( c, propagated) {
314+ ( '0' ..='8' , false ) => ( c as u8 + 1 ) as char ,
315+ ( _, false ) => '0' ,
316+ ( _, true ) => c,
317+ } ;
318+ replacement. push ( d) ;
319+ if d != '0' {
320+ propagated = true ;
321+ }
322+ }
323+ digits. clear ( ) ;
324+ let replacement = if propagated {
325+ replacement. as_str ( )
326+ } else {
327+ digits. push ( '1' ) ;
328+ & replacement. as_str ( ) [ 1 ..]
329+ } ;
330+ for c in replacement. chars ( ) . rev ( ) {
331+ digits. push ( c) ;
332+ }
333+ !propagated
334+ } else {
335+ digits. push ( digit as char ) ;
336+ false
337+ }
338+ } else {
339+ digits. push_str ( & "0" . repeat ( precision - digits. len ( ) ) ) ;
340+ false
341+ }
342+ }
343+
344+ /// Mimics JavaScript's `Number.prototype.toPrecision()` method
345+ ///
346+ /// The `toPrecision()` method returns a string representing the Number object to the specified precision.
347+ ///
348+ /// More information:
349+ /// - [ECMAScript reference][spec]
350+ /// - [MDN documentation][mdn]
351+ ///
352+ /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision
353+ /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision
354+ #[ expect( clippy:: cast_possible_truncation) ]
355+ #[ expect( clippy:: cast_possible_wrap) ]
356+ #[ expect( clippy:: cast_sign_loss) ]
357+ pub fn to_precision ( mut num : f64 , precision : usize ) -> String {
358+ // Validate precision range (1-100)
359+ debug_assert ! ( ( 1 ..=100 ) . contains( & precision) , "Precision must be between 1 and 100" ) ;
360+
361+ // Handle non-finite numbers
362+ if !num. is_finite ( ) {
363+ if num. is_nan ( ) {
364+ return "NaN" . to_string ( ) ;
365+ } else if num. is_infinite ( ) {
366+ return if num. is_sign_positive ( ) { "Infinity" } else { "-Infinity" } . to_string ( ) ;
367+ }
368+ }
369+
370+ let precision_i32 = precision as i32 ;
371+
372+ // Handle sign
373+ let mut prefix = String :: new ( ) ;
374+ if num < 0.0 {
375+ prefix. push ( '-' ) ;
376+ num = -num;
377+ }
378+
379+ let mut suffix: String ;
380+ let mut exponent: i32 ;
381+
382+ // Handle zero
383+ if num == 0.0 {
384+ suffix = "0" . repeat ( precision) ;
385+ exponent = 0 ;
386+ } else {
387+ // Format with maximum precision to get all digits
388+ suffix = format ! ( "{num:.100}" ) ;
389+
390+ // Calculate exponent
391+ exponent = flt_str_to_exp ( & suffix) ;
392+
393+ // Extract relevant digits only
394+ if exponent < 0 {
395+ suffix = suffix. split_off ( ( 1 - exponent) as usize ) ;
396+ } else if let Some ( n) = suffix. find ( '.' ) {
397+ suffix. remove ( n) ;
398+ }
399+
400+ // Round to the specified precision
401+ if round_to_precision ( & mut suffix, precision) {
402+ exponent += 1 ;
403+ }
404+
405+ // Decide between scientific and fixed notation
406+ let great_exp = exponent >= precision_i32;
407+ if exponent < -6 || great_exp {
408+ // Use scientific notation
409+ if precision > 1 {
410+ suffix. insert ( 1 , '.' ) ;
411+ }
412+ suffix. push ( 'e' ) ;
413+ if great_exp {
414+ suffix. push ( '+' ) ;
415+ }
416+ suffix. push_str ( & exponent. to_string ( ) ) ;
417+
418+ return prefix + & suffix;
419+ }
420+ }
421+
422+ // Use fixed-point notation
423+ let e_inc = exponent + 1 ;
424+ if e_inc == precision_i32 {
425+ return prefix + & suffix;
426+ }
427+
428+ if exponent >= 0 {
429+ suffix. insert ( e_inc as usize , '.' ) ;
430+ } else {
431+ prefix. push ( '0' ) ;
432+ prefix. push ( '.' ) ;
433+ prefix. push_str ( & "0" . repeat ( -e_inc as usize ) ) ;
434+ }
435+
436+ prefix + & suffix
437+ }
438+
280439#[ test]
281440fn test ( ) {
282441 use crate :: tester:: Tester ;
@@ -350,6 +509,7 @@ fn test() {
350509 ( "var a = Infinity" , None ) ,
351510 ( "var a = 480.00" , None ) ,
352511 ( "var a = -30.00" , None ) ,
512+ ( "(1000000000000000128).toFixed(0)" , None ) ,
353513 ] ;
354514
355515 let fail = vec ! [
0 commit comments