1 | use std::ops::{Bound::*, RangeBounds}; |
2 | |
3 | use glib::translate::*; |
4 | use gst::Caps; |
5 | |
6 | use crate::VideoFormat; |
7 | |
8 | pub struct VideoCapsBuilder<T> { |
9 | builder: gst::caps::Builder<T>, |
10 | } |
11 | |
12 | impl VideoCapsBuilder<gst::caps::NoFeature> { |
13 | // rustdoc-stripper-ignore-next |
14 | /// Constructs an `VideoCapsBuilder` for the "video/x-raw" encoding. |
15 | /// |
16 | /// If left unchanged, the resulting `Caps` will be initialized with: |
17 | /// - "video/x-raw" encoding. |
18 | /// - all available formats. |
19 | /// - maximum width range. |
20 | /// - maximum height range. |
21 | /// |
22 | /// Use [`VideoCapsBuilder::for_encoding`] to specify another encoding. |
23 | pub fn new() -> Self { |
24 | assert_initialized_main_thread!(); |
25 | let builder = Caps::builder(glib::gstr!("video/x-raw" )); |
26 | let builder = VideoCapsBuilder { builder }; |
27 | builder |
28 | .format_list(VideoFormat::iter_raw()) |
29 | .width_range(..) |
30 | .height_range(..) |
31 | .framerate_range(..) |
32 | } |
33 | |
34 | // rustdoc-stripper-ignore-next |
35 | /// Constructs an `VideoCapsBuilder` for the specified encoding. |
36 | /// |
37 | /// The resulting `Caps` will use the `encoding` argument as name |
38 | /// and will not contain any additional fields unless explicitly added. |
39 | pub fn for_encoding(encoding: impl IntoGStr) -> Self { |
40 | assert_initialized_main_thread!(); |
41 | VideoCapsBuilder { |
42 | builder: Caps::builder(encoding), |
43 | } |
44 | } |
45 | |
46 | pub fn any_features(self) -> VideoCapsBuilder<gst::caps::HasFeatures> { |
47 | VideoCapsBuilder { |
48 | builder: self.builder.any_features(), |
49 | } |
50 | } |
51 | |
52 | pub fn features( |
53 | self, |
54 | features: impl IntoIterator<Item = impl IntoGStr>, |
55 | ) -> VideoCapsBuilder<gst::caps::HasFeatures> { |
56 | VideoCapsBuilder { |
57 | builder: self.builder.features(features), |
58 | } |
59 | } |
60 | } |
61 | |
62 | impl Default for VideoCapsBuilder<gst::caps::NoFeature> { |
63 | fn default() -> Self { |
64 | Self::new() |
65 | } |
66 | } |
67 | |
68 | impl<T> VideoCapsBuilder<T> { |
69 | pub fn format(self, format: VideoFormat) -> Self { |
70 | Self { |
71 | builder: self.builder.field(glib::gstr!("format" ), format.to_str()), |
72 | } |
73 | } |
74 | |
75 | pub fn format_if(self, format: VideoFormat, predicate: bool) -> Self { |
76 | if predicate { |
77 | self.format(format) |
78 | } else { |
79 | self |
80 | } |
81 | } |
82 | |
83 | pub fn format_if_some(self, format: Option<VideoFormat>) -> Self { |
84 | if let Some(format) = format { |
85 | self.format(format) |
86 | } else { |
87 | self |
88 | } |
89 | } |
90 | |
91 | pub fn format_list(self, formats: impl IntoIterator<Item = VideoFormat>) -> Self { |
92 | Self { |
93 | builder: self.builder.field( |
94 | glib::gstr!("format" ), |
95 | gst::List::new(formats.into_iter().map(|f| f.to_str())), |
96 | ), |
97 | } |
98 | } |
99 | |
100 | pub fn format_list_if( |
101 | self, |
102 | formats: impl IntoIterator<Item = VideoFormat>, |
103 | predicate: bool, |
104 | ) -> Self { |
105 | if predicate { |
106 | self.format_list(formats) |
107 | } else { |
108 | self |
109 | } |
110 | } |
111 | |
112 | pub fn format_list_if_some( |
113 | self, |
114 | formats: Option<impl IntoIterator<Item = VideoFormat>>, |
115 | ) -> Self { |
116 | if let Some(formats) = formats { |
117 | self.format_list(formats) |
118 | } else { |
119 | self |
120 | } |
121 | } |
122 | |
123 | pub fn format_list_if_not_empty(self, formats: impl IntoIterator<Item = VideoFormat>) -> Self { |
124 | let mut formats = formats.into_iter().peekable(); |
125 | if formats.peek().is_some() { |
126 | self.format_list(formats) |
127 | } else { |
128 | self |
129 | } |
130 | } |
131 | |
132 | pub fn width(self, width: i32) -> Self { |
133 | Self { |
134 | builder: self.builder.field(glib::gstr!("width" ), width), |
135 | } |
136 | } |
137 | |
138 | pub fn width_if(self, width: i32, predicate: bool) -> Self { |
139 | if predicate { |
140 | self.width(width) |
141 | } else { |
142 | self |
143 | } |
144 | } |
145 | |
146 | pub fn width_if_some(self, width: Option<i32>) -> Self { |
147 | if let Some(width) = width { |
148 | self.width(width) |
149 | } else { |
150 | self |
151 | } |
152 | } |
153 | |
154 | pub fn width_range(self, widths: impl RangeBounds<i32>) -> Self { |
155 | let (start, end) = range_bounds_i32_start_end(widths); |
156 | let gst_widths: gst::IntRange<i32> = gst::IntRange::new(start, end); |
157 | Self { |
158 | builder: self.builder.field(glib::gstr!("width" ), gst_widths), |
159 | } |
160 | } |
161 | |
162 | pub fn width_range_if_some(self, widths: Option<impl RangeBounds<i32>>) -> Self { |
163 | if let Some(widths) = widths { |
164 | self.width_range(widths) |
165 | } else { |
166 | self |
167 | } |
168 | } |
169 | |
170 | pub fn width_range_if(self, widths: impl RangeBounds<i32>, predicate: bool) -> Self { |
171 | if predicate { |
172 | self.width_range(widths) |
173 | } else { |
174 | self |
175 | } |
176 | } |
177 | |
178 | pub fn width_list(self, widths: impl IntoIterator<Item = i32>) -> Self { |
179 | Self { |
180 | builder: self |
181 | .builder |
182 | .field(glib::gstr!("width" ), gst::List::new(widths)), |
183 | } |
184 | } |
185 | |
186 | pub fn width_list_if(self, widths: impl IntoIterator<Item = i32>, predicate: bool) -> Self { |
187 | if predicate { |
188 | self.width_list(widths) |
189 | } else { |
190 | self |
191 | } |
192 | } |
193 | |
194 | pub fn width_list_if_some(self, widths: Option<impl IntoIterator<Item = i32>>) -> Self { |
195 | if let Some(widths) = widths { |
196 | self.width_list(widths) |
197 | } else { |
198 | self |
199 | } |
200 | } |
201 | |
202 | pub fn width_list_if_not_empty(self, widths: impl IntoIterator<Item = i32>) -> Self { |
203 | let mut widths = widths.into_iter().peekable(); |
204 | if widths.peek().is_some() { |
205 | self.width_list(widths) |
206 | } else { |
207 | self |
208 | } |
209 | } |
210 | |
211 | pub fn height(self, height: i32) -> Self { |
212 | Self { |
213 | builder: self.builder.field(glib::gstr!("height" ), height), |
214 | } |
215 | } |
216 | |
217 | pub fn height_if(self, height: i32, predicate: bool) -> Self { |
218 | if predicate { |
219 | self.height(height) |
220 | } else { |
221 | self |
222 | } |
223 | } |
224 | |
225 | pub fn height_if_some(self, height: Option<i32>) -> Self { |
226 | if let Some(height) = height { |
227 | self.height(height) |
228 | } else { |
229 | self |
230 | } |
231 | } |
232 | |
233 | pub fn height_range(self, heights: impl RangeBounds<i32>) -> Self { |
234 | let (start, end) = range_bounds_i32_start_end(heights); |
235 | let gst_heights: gst::IntRange<i32> = gst::IntRange::new(start, end); |
236 | Self { |
237 | builder: self.builder.field(glib::gstr!("height" ), gst_heights), |
238 | } |
239 | } |
240 | |
241 | pub fn height_range_if(self, heights: impl RangeBounds<i32>, predicate: bool) -> Self { |
242 | if predicate { |
243 | self.height_range(heights) |
244 | } else { |
245 | self |
246 | } |
247 | } |
248 | |
249 | pub fn height_range_if_some(self, heights: Option<impl RangeBounds<i32>>) -> Self { |
250 | if let Some(heights) = heights { |
251 | self.height_range(heights) |
252 | } else { |
253 | self |
254 | } |
255 | } |
256 | |
257 | pub fn height_list(self, heights: impl IntoIterator<Item = i32>) -> Self { |
258 | Self { |
259 | builder: self |
260 | .builder |
261 | .field(glib::gstr!("height" ), gst::List::new(heights)), |
262 | } |
263 | } |
264 | |
265 | pub fn height_list_if(self, heights: impl IntoIterator<Item = i32>, predicate: bool) -> Self { |
266 | if predicate { |
267 | self.height_list(heights) |
268 | } else { |
269 | self |
270 | } |
271 | } |
272 | |
273 | pub fn height_list_if_some(self, heights: Option<impl IntoIterator<Item = i32>>) -> Self { |
274 | if let Some(heights) = heights { |
275 | self.height_list(heights) |
276 | } else { |
277 | self |
278 | } |
279 | } |
280 | |
281 | pub fn height_list_if_not_empty(self, heights: impl IntoIterator<Item = i32>) -> Self { |
282 | let mut heights = heights.into_iter().peekable(); |
283 | if heights.peek().is_some() { |
284 | self.height_list(heights) |
285 | } else { |
286 | self |
287 | } |
288 | } |
289 | |
290 | pub fn framerate(self, framerate: gst::Fraction) -> Self { |
291 | Self { |
292 | builder: self.builder.field(glib::gstr!("framerate" ), framerate), |
293 | } |
294 | } |
295 | |
296 | pub fn framerate_if(self, framerate: gst::Fraction, predicate: bool) -> Self { |
297 | if predicate { |
298 | self.framerate(framerate) |
299 | } else { |
300 | self |
301 | } |
302 | } |
303 | |
304 | pub fn framerate_if_some(self, framerate: Option<gst::Fraction>) -> Self { |
305 | if let Some(framerate) = framerate { |
306 | self.framerate(framerate) |
307 | } else { |
308 | self |
309 | } |
310 | } |
311 | |
312 | pub fn framerate_range(self, framerates: impl RangeBounds<gst::Fraction>) -> Self { |
313 | let start = match framerates.start_bound() { |
314 | Unbounded => gst::Fraction::new(0, 1), |
315 | Excluded(n) => next_fraction(*n), |
316 | Included(n) => { |
317 | assert!(n.numer() >= 0); |
318 | *n |
319 | } |
320 | }; |
321 | let end = match framerates.end_bound() { |
322 | Unbounded => gst::Fraction::new(i32::MAX, 1), |
323 | Excluded(n) => previous_fraction(*n), |
324 | Included(n) => { |
325 | assert!(n.numer() >= 0); |
326 | *n |
327 | } |
328 | }; |
329 | assert!(start <= end); |
330 | let framerates: gst::FractionRange = gst::FractionRange::new(start, end); |
331 | Self { |
332 | builder: self.builder.field(glib::gstr!("framerate" ), framerates), |
333 | } |
334 | } |
335 | |
336 | pub fn framerate_range_if( |
337 | self, |
338 | framerates: impl RangeBounds<gst::Fraction>, |
339 | predicate: bool, |
340 | ) -> Self { |
341 | if predicate { |
342 | self.framerate_range(framerates) |
343 | } else { |
344 | self |
345 | } |
346 | } |
347 | |
348 | pub fn framerate_range_if_some( |
349 | self, |
350 | framerates: Option<impl RangeBounds<gst::Fraction>>, |
351 | ) -> Self { |
352 | if let Some(framerates) = framerates { |
353 | self.framerate_range(framerates) |
354 | } else { |
355 | self |
356 | } |
357 | } |
358 | |
359 | pub fn framerate_list(self, framerates: impl IntoIterator<Item = gst::Fraction>) -> Self { |
360 | Self { |
361 | builder: self |
362 | .builder |
363 | .field(glib::gstr!("framerate" ), gst::List::new(framerates)), |
364 | } |
365 | } |
366 | |
367 | pub fn framerate_list_if( |
368 | self, |
369 | framerates: impl IntoIterator<Item = gst::Fraction>, |
370 | predicate: bool, |
371 | ) -> Self { |
372 | if predicate { |
373 | self.framerate_list(framerates) |
374 | } else { |
375 | self |
376 | } |
377 | } |
378 | |
379 | pub fn framerate_list_if_some( |
380 | self, |
381 | framerates: Option<impl IntoIterator<Item = gst::Fraction>>, |
382 | ) -> Self { |
383 | if let Some(framerates) = framerates { |
384 | self.framerate_list(framerates) |
385 | } else { |
386 | self |
387 | } |
388 | } |
389 | |
390 | pub fn framerate_list_if_not_empty( |
391 | self, |
392 | framerates: impl IntoIterator<Item = gst::Fraction>, |
393 | ) -> Self { |
394 | let mut framerates = framerates.into_iter().peekable(); |
395 | if framerates.peek().is_some() { |
396 | self.framerate_list(framerates) |
397 | } else { |
398 | self |
399 | } |
400 | } |
401 | |
402 | pub fn pixel_aspect_ratio(self, pixel_aspect_ratio: gst::Fraction) -> Self { |
403 | Self { |
404 | builder: self.builder.field("pixel-aspect-ratio" , pixel_aspect_ratio), |
405 | } |
406 | } |
407 | |
408 | pub fn pixel_aspect_ratio_if(self, pixel_aspect_ratio: gst::Fraction, predicate: bool) -> Self { |
409 | if predicate { |
410 | self.pixel_aspect_ratio(pixel_aspect_ratio) |
411 | } else { |
412 | self |
413 | } |
414 | } |
415 | |
416 | pub fn pixel_aspect_ratio_if_some(self, pixel_aspect_ratio: Option<gst::Fraction>) -> Self { |
417 | if let Some(pixel_aspect_ratio) = pixel_aspect_ratio { |
418 | self.pixel_aspect_ratio(pixel_aspect_ratio) |
419 | } else { |
420 | self |
421 | } |
422 | } |
423 | |
424 | pub fn pixel_aspect_ratio_range( |
425 | self, |
426 | pixel_aspect_ratios: impl RangeBounds<gst::Fraction>, |
427 | ) -> Self { |
428 | let start = match pixel_aspect_ratios.start_bound() { |
429 | Unbounded => gst::Fraction::new(1, i32::MAX), |
430 | Excluded(n) => next_fraction(*n), |
431 | Included(n) => { |
432 | assert!(n.numer() >= 0); |
433 | *n |
434 | } |
435 | }; |
436 | let end = match pixel_aspect_ratios.end_bound() { |
437 | Unbounded => gst::Fraction::new(i32::MAX, 1), |
438 | Excluded(n) => previous_fraction(*n), |
439 | Included(n) => { |
440 | assert!(n.numer() >= 0); |
441 | *n |
442 | } |
443 | }; |
444 | assert!(start <= end); |
445 | let pixel_aspect_ratios: gst::FractionRange = gst::FractionRange::new(start, end); |
446 | Self { |
447 | builder: self |
448 | .builder |
449 | .field("pixel-aspect-ratio" , pixel_aspect_ratios), |
450 | } |
451 | } |
452 | |
453 | pub fn pixel_aspect_ratio_range_if( |
454 | self, |
455 | pixel_aspect_ratios: impl RangeBounds<gst::Fraction>, |
456 | predicate: bool, |
457 | ) -> Self { |
458 | if predicate { |
459 | self.pixel_aspect_ratio_range(pixel_aspect_ratios) |
460 | } else { |
461 | self |
462 | } |
463 | } |
464 | |
465 | pub fn pixel_aspect_ratio_range_if_some( |
466 | self, |
467 | pixel_aspect_ratios: Option<impl RangeBounds<gst::Fraction>>, |
468 | ) -> Self { |
469 | if let Some(pixel_aspect_ratios) = pixel_aspect_ratios { |
470 | self.pixel_aspect_ratio_range(pixel_aspect_ratios) |
471 | } else { |
472 | self |
473 | } |
474 | } |
475 | |
476 | pub fn pixel_aspect_ratio_list( |
477 | self, |
478 | pixel_aspect_ratios: impl IntoIterator<Item = gst::Fraction>, |
479 | ) -> Self { |
480 | Self { |
481 | builder: self |
482 | .builder |
483 | .field("pixel-aspect-ratio" , gst::List::new(pixel_aspect_ratios)), |
484 | } |
485 | } |
486 | |
487 | pub fn pixel_aspect_ratio_list_if( |
488 | self, |
489 | pixel_aspect_ratios: impl IntoIterator<Item = gst::Fraction>, |
490 | predicate: bool, |
491 | ) -> Self { |
492 | if predicate { |
493 | self.pixel_aspect_ratio_list(pixel_aspect_ratios) |
494 | } else { |
495 | self |
496 | } |
497 | } |
498 | |
499 | pub fn pixel_aspect_ratio_list_if_some( |
500 | self, |
501 | pixel_aspect_ratios: Option<impl IntoIterator<Item = gst::Fraction>>, |
502 | ) -> Self { |
503 | if let Some(pixel_aspect_ratios) = pixel_aspect_ratios { |
504 | self.pixel_aspect_ratio_list(pixel_aspect_ratios) |
505 | } else { |
506 | self |
507 | } |
508 | } |
509 | |
510 | pub fn pixel_aspect_ratio_list_if_not_empty( |
511 | self, |
512 | pixel_aspect_ratios: impl IntoIterator<Item = gst::Fraction>, |
513 | ) -> Self { |
514 | let mut pixel_aspect_ratios = pixel_aspect_ratios.into_iter().peekable(); |
515 | if pixel_aspect_ratios.peek().is_some() { |
516 | self.pixel_aspect_ratio_list(pixel_aspect_ratios) |
517 | } else { |
518 | self |
519 | } |
520 | } |
521 | |
522 | // rustdoc-stripper-ignore-next |
523 | /// Sets field `name` to the given value `value`. |
524 | /// |
525 | /// Overrides any default or previously defined value for `name`. |
526 | #[inline ] |
527 | pub fn field(self, name: impl IntoGStr, value: impl Into<glib::Value> + Send) -> Self { |
528 | Self { |
529 | builder: self.builder.field(name, value), |
530 | } |
531 | } |
532 | |
533 | gst::impl_builder_gvalue_extra_setters!(field); |
534 | |
535 | #[must_use ] |
536 | pub fn build(self) -> gst::Caps { |
537 | self.builder.build() |
538 | } |
539 | } |
540 | |
541 | fn range_bounds_i32_start_end(range: impl RangeBounds<i32>) -> (i32, i32) { |
542 | skip_assert_initialized!(); |
543 | let start: i32 = match range.start_bound() { |
544 | Unbounded => 1, |
545 | Excluded(n: &i32) => n + 1, |
546 | Included(n: &i32) => *n, |
547 | }; |
548 | let end: i32 = match range.end_bound() { |
549 | Unbounded => i32::MAX, |
550 | Excluded(n: &i32) => n - 1, |
551 | Included(n: &i32) => *n, |
552 | }; |
553 | (start, end) |
554 | } |
555 | |
556 | // https://math.stackexchange.com/questions/39582/how-to-compute-next-previous-representable-rational-number/3798608#3798608 |
557 | |
558 | /* Extended Euclidean Algorithm: computes (g, x, y), |
559 | * such that a*x + b*y = g = gcd(a, b) >= 0. */ |
560 | fn xgcd(mut a: i64, mut b: i64) -> (i64, i64, i64) { |
561 | skip_assert_initialized!(); |
562 | let mut x0: i64 = 0i64; |
563 | let mut x1: i64 = 1i64; |
564 | let mut y0: i64 = 1i64; |
565 | let mut y1: i64 = 0i64; |
566 | while a != 0 { |
567 | let q: i64; |
568 | (q, a, b) = (b / a, b % a, a); |
569 | (y0, y1) = (y1, y0 - q * y1); |
570 | (x0, x1) = (x1, x0 - q * x1); |
571 | } |
572 | if b >= 0 { |
573 | (b, x0, y0) |
574 | } else { |
575 | (-b, -x0, -y0) |
576 | } |
577 | } |
578 | |
579 | /* Computes the neighbours of p/q in the Farey sequence of order n. */ |
580 | fn farey_neighbours(p: i32, q: i32) -> (i32, i32, i32, i32) { |
581 | skip_assert_initialized!(); |
582 | let n: i64 = i32::MAX as i64; |
583 | assert!(q != 0); |
584 | let mut p: i64 = p as i64; |
585 | let mut q: i64 = q as i64; |
586 | if q < 0 { |
587 | p = -p; |
588 | q = -q; |
589 | } |
590 | let (g: i64, r: i64, _) = xgcd(a:p, b:q); |
591 | p /= g; |
592 | q /= g; |
593 | let b: i64 = ((n - r) / q) * q + r; |
594 | let a: i64 = (b * p - 1) / q; |
595 | let d: i64 = ((n + r) / q) * q - r; |
596 | let c: i64 = (d * p + 1) / q; |
597 | (a as i32, b as i32, c as i32, d as i32) |
598 | } |
599 | |
600 | fn previous_fraction(fraction: gst::Fraction) -> gst::Fraction { |
601 | skip_assert_initialized!(); |
602 | let num: i32 = fraction.numer(); |
603 | let den: i32 = fraction.denom(); |
604 | let (new_num: i32, new_den: i32); |
605 | if num < den { |
606 | (new_num, new_den, _, _) = farey_neighbours(p:num, q:den); |
607 | } else { |
608 | (_, _, new_den, new_num) = farey_neighbours(p:den, q:num); |
609 | } |
610 | gst::Fraction::new(numer:new_num, denom:new_den) |
611 | } |
612 | |
613 | fn next_fraction(fraction: gst::Fraction) -> gst::Fraction { |
614 | skip_assert_initialized!(); |
615 | let num: i32 = fraction.numer(); |
616 | let den: i32 = fraction.denom(); |
617 | let (new_num: i32, new_den: i32); |
618 | if num < den { |
619 | (_, _, new_num, new_den) = farey_neighbours(p:num, q:den); |
620 | } else { |
621 | (new_den, new_num, _, _) = farey_neighbours(p:den, q:num); |
622 | } |
623 | gst::Fraction::new(numer:new_num, denom:new_den) |
624 | } |
625 | |
626 | #[cfg (test)] |
627 | mod tests { |
628 | use super::{next_fraction, previous_fraction, VideoCapsBuilder}; |
629 | |
630 | #[test ] |
631 | fn default_encoding() { |
632 | gst::init().unwrap(); |
633 | let caps = VideoCapsBuilder::new().build(); |
634 | assert_eq!(caps.structure(0).unwrap().name(), "video/x-raw" ); |
635 | } |
636 | |
637 | #[test ] |
638 | fn explicit_encoding() { |
639 | gst::init().unwrap(); |
640 | let caps = VideoCapsBuilder::for_encoding("video/mpeg" ).build(); |
641 | assert_eq!(caps.structure(0).unwrap().name(), "video/mpeg" ); |
642 | } |
643 | |
644 | #[test ] |
645 | fn test_0_1_fraction() { |
646 | gst::init().unwrap(); |
647 | let zero_over_one = gst::Fraction::new(0, 1); |
648 | let prev = previous_fraction(zero_over_one); |
649 | assert_eq!(prev.numer(), -1); |
650 | assert_eq!(prev.denom(), i32::MAX); |
651 | let next = next_fraction(zero_over_one); |
652 | assert_eq!(next.numer(), 1); |
653 | assert_eq!(next.denom(), i32::MAX); |
654 | } |
655 | |
656 | #[test ] |
657 | fn test_25_1() { |
658 | gst::init().unwrap(); |
659 | let twentyfive = gst::Fraction::new(25, 1); |
660 | let next = next_fraction(twentyfive); |
661 | //25.000000011641532 |
662 | assert_eq!(next.numer(), 2147483626); |
663 | assert_eq!(next.denom(), 85899345); |
664 | let prev = previous_fraction(twentyfive); |
665 | //24.999999988358468 |
666 | assert_eq!(prev.numer(), 2147483624); |
667 | assert_eq!(prev.denom(), 85899345); |
668 | } |
669 | #[test ] |
670 | fn test_1_25() { |
671 | gst::init().unwrap(); |
672 | let twentyfive = gst::Fraction::new(1, 25); |
673 | let next = next_fraction(twentyfive); |
674 | //0.040000000018626 |
675 | assert_eq!(next.numer(), 85899345); |
676 | assert_eq!(next.denom(), 2147483624); |
677 | let prev = previous_fraction(twentyfive); |
678 | //0.039999999981374 |
679 | assert_eq!(prev.numer(), 85899345); |
680 | assert_eq!(prev.denom(), 2147483626); |
681 | } |
682 | } |
683 | |