1use super::lower_interval;
52use crate::model::{Bar, BarClock, Timeframe};
53use crate::studies::palette;
54use crate::studies::stepper::Stepper;
55use crate::studies::{
56 Indicator, IndicatorValue, IndicatorValues, InputKind, InputValue, StudyCell, StudyData,
57 StudyDataRequest, StudyInput, StudyTable,
58};
59use chrono::{DateTime, TimeDelta, Utc};
60use egui::Color32;
61use std::collections::VecDeque;
62
63#[derive(Clone)]
74pub struct Volume24h {
75 values: IndicatorValues,
76 stepper: Stepper<State>,
77 clock: BarClock,
79 from_candles: bool,
81 quote_units: bool,
82 currency: Option<String>,
83 precision: Option<usize>,
84 table: Option<StudyTable>,
86 color: Color32,
87 visible: bool,
88}
89
90#[derive(Clone, Default)]
91struct State {
92 window: VecDeque<(DateTime<Utc>, Option<f64>)>,
94 sum: f64,
95 invalid: usize,
96}
97
98impl Volume24h {
99 pub fn new() -> Self {
101 Self {
102 values: IndicatorValues::new(),
103 stepper: Stepper::default(),
104 clock: BarClock::new(Timeframe::Min1),
105 from_candles: false,
106 quote_units: false,
107 currency: None,
108 precision: None,
109 table: None,
110 color: palette::BLUE,
111 visible: true,
112 }
113 }
114
115 fn clear(&mut self) {
116 self.values.clear();
117 self.stepper = Stepper::default();
118 self.from_candles = false;
119 self.update_table();
120 }
121
122 fn amount(bar: &Bar, quote_units: bool) -> Option<f64> {
123 let amount = if quote_units {
124 if !bar.close.is_finite() || bar.volume < 0.0 {
125 return None;
126 }
127 bar.volume * bar.close
128 } else {
129 bar.volume
130 };
131 amount.is_finite().then_some(amount)
132 }
133
134 fn unit(&self) -> &str {
135 if self.quote_units {
136 self.currency
137 .as_deref()
138 .unwrap_or("quote currency unavailable")
139 } else {
140 "supplied units"
141 }
142 }
143
144 fn update_table(&mut self) {
145 let unavailable = self
146 .values
147 .len()
148 .checked_sub(1)
149 .is_some_and(|i| self.quote_units && self.values.value(0, i).is_none());
150 self.table = unavailable.then(|| StudyTable {
151 title: "24-hour Volume".into(),
152 columns: vec![self.line_names().remove(0)],
153 rows: vec![vec![StudyCell::Text(if self.currency.is_none() {
154 "Quote currency unavailable".into()
155 } else {
156 "Quote amount unavailable".into()
157 })]],
158 });
159 }
160
161 fn lower_timeframe(&self) -> Option<Timeframe> {
164 let span = self.clock.timeframe()?.duration_ms();
165 let day = Timeframe::Day1.duration_ms();
166 if span > 0 && span <= day && day % span == 0 {
167 return None;
168 }
169 lower_interval::lower_timeframe(&self.clock, None)
170 }
171
172 fn sum_candles(&mut self, data: &[Bar], from: usize, lower: Timeframe, candles: &[Bar]) {
175 let from = if self.from_candles { from } else { 0 };
176 self.from_candles = true;
177 if from == 0 {
178 self.values = IndicatorValues::missing(data.len());
179 } else {
180 self.values.truncate(from);
181 self.values.resize(data.len(), IndicatorValue::None);
182 }
183 let ranges = lower_interval::nest(&self.clock, lower, data, candles, from);
184 for (i, range) in (from..).zip(ranges) {
185 if range.is_empty() || (!self.quote_units && !data[i].close.is_finite()) {
186 continue;
187 }
188 let last = range.end - 1;
189 let end = candles[last]
190 .time
191 .checked_add_signed(lower.duration())
192 .unwrap_or(DateTime::<Utc>::MAX_UTC);
193 let start = end
194 .checked_sub_signed(TimeDelta::days(1))
195 .unwrap_or(DateTime::<Utc>::MIN_UTC);
196 let first = candles[..last].partition_point(|candle| candle.time < start);
197 let sum = if self.quote_units {
201 candles[first..range.end]
202 .iter()
203 .map(|candle| Self::amount(candle, true))
204 .sum::<Option<f64>>()
205 .filter(|sum| sum.is_finite())
206 .filter(|_| self.currency.is_some())
207 } else {
208 Some(
209 candles[first..range.end]
210 .iter()
211 .map(|candle| candle.volume)
212 .sum(),
213 )
214 };
215 if let Some(sum) = sum {
216 self.values.set_value(0, i, sum);
217 }
218 }
219 self.update_table();
220 }
221}
222
223impl Default for Volume24h {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl Indicator for Volume24h {
230 fn inputs(&self) -> Vec<StudyInput> {
231 vec![
232 StudyInput::new(
233 "units",
234 "Units",
235 InputValue::Choice(usize::from(self.quote_units)),
236 InputKind::Choice(vec![
237 "Supplied volume".into(),
238 "Quote currency (volume in base units)".into(),
239 ]),
240 )
241 .with_default(InputValue::Choice(0)),
242 ]
243 }
244
245 fn set_input(&mut self, id: &str, value: InputValue) -> Result<(), crate::studies::StudyError> {
246 if id != "units" {
247 return Err(crate::studies::StudyError::UnknownInput(id.into()));
248 }
249 let InputValue::Choice(units @ 0..=1) = value else {
250 return Err(crate::studies::StudyError::invalid_input(id));
251 };
252 self.quote_units = units == 1;
253 self.clear();
254 Ok(())
255 }
256
257 fn name(&self) -> &str {
258 "24-hour Volume"
259 }
260
261 fn desc(&self) -> &str {
262 "24-hour Volume - Volume traded over the last 24 hours, on any interval"
263 }
264
265 fn value_format(&self) -> crate::scales::StudyFormat {
266 if self.quote_units {
267 crate::scales::StudyFormat::Fixed(2)
268 } else {
269 crate::scales::StudyFormat::Volume
270 }
271 }
272
273 fn precision(&self) -> Option<usize> {
274 self.precision.or(self.quote_units.then_some(2))
275 }
276
277 fn set_label_precision(&mut self, precision: Option<usize>) {
278 self.precision = precision;
279 self.update_table();
280 }
281
282 fn format_value(&self, value: f64) -> String {
283 let value = self.value_format().format(value, self.precision());
284 match &self.currency {
285 Some(currency) if self.quote_units => format!("{value} {currency}"),
286 _ => value,
287 }
288 }
289
290 fn line_names(&self) -> Vec<String> {
291 vec![format!(
292 "{} ({})",
293 if self.quote_units { "Amount" } else { "Volume" },
294 self.unit()
295 )]
296 }
297
298 fn table(&self) -> Option<&StudyTable> {
299 self.table.as_ref()
300 }
301
302 fn set_instrument(&mut self, instrument: Option<&crate::model::Instrument>) -> bool {
303 let currency = instrument
304 .and_then(|i| i.currency.as_deref())
305 .map(str::trim)
306 .filter(|s| !s.is_empty());
307 if self.currency.as_deref() == currency {
308 return false;
309 }
310 self.currency = currency.map(str::to_owned);
311 self.clear();
312 true
313 }
314
315 fn plot_style(&self, _line: usize) -> crate::studies::PlotStyle {
316 crate::studies::PlotStyle {
317 color: self.color,
318 width: 1.0,
319 pattern: crate::model::LineStyle::Solid,
320 plot_type: crate::studies::PlotType::Columns,
321 visible: true,
322 }
323 }
324
325 fn calculate(&mut self, data: &[Bar]) {
326 self.update(data, 0);
327 }
328
329 fn update(&mut self, data: &[Bar], from: usize) {
330 if std::mem::take(&mut self.from_candles) {
331 self.values.clear();
333 self.stepper = Stepper::default();
334 }
335 let long_bars = self
336 .clock
337 .timeframe()
338 .is_some_and(|timeframe| timeframe.duration_ms() > Timeframe::Day1.duration_ms());
339 let quote_units = self.quote_units;
340 let available = !quote_units || self.currency.is_some();
341 self.stepper
342 .run(data, from, &mut self.values, |state, bar| {
343 if long_bars || !available || (!quote_units && !bar.volume.is_finite()) {
344 return IndicatorValue::None;
345 }
346 let start = bar
347 .time
348 .checked_sub_signed(TimeDelta::days(1))
349 .unwrap_or(DateTime::<Utc>::MIN_UTC);
350 let mut largest = 0.0_f64;
351 while let Some(&(time, volume)) = state.window.front()
352 && time <= start
353 {
354 state.window.pop_front();
355 if let Some(volume) = volume {
356 state.sum -= volume;
357 largest = largest.max(volume.abs());
358 } else {
359 state.invalid -= 1;
360 }
361 }
362 if largest > state.sum.abs() {
363 state.sum = state.window.iter().filter_map(|&(_, volume)| volume).sum();
366 }
367 let amount = Self::amount(bar, quote_units);
368 state.window.push_back((bar.time, amount));
369 if let Some(amount) = amount {
370 state.sum += amount;
371 } else {
372 state.invalid += 1;
373 }
374 if !state.sum.is_finite() {
376 state.sum = state.window.iter().filter_map(|&(_, volume)| volume).sum();
377 }
378 if state.invalid == 0 && state.sum.is_finite() {
379 IndicatorValue::Single(state.sum)
380 } else {
381 IndicatorValue::None
382 }
383 });
384 self.update_table();
385 }
386
387 fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
388 let changed = self.clock != *clock;
389 if changed {
390 self.clock = clock.clone();
391 self.clear();
392 }
393 changed
394 }
395
396 fn data_requests(&self) -> Vec<StudyDataRequest> {
397 self.lower_timeframe()
398 .map(lower_interval::request)
399 .into_iter()
400 .collect()
401 }
402
403 fn update_data(&mut self, data: &[Bar], from: usize, sources: &[StudyData<'_>]) {
404 let Some(lower) = self.lower_timeframe() else {
405 return self.update(data, from);
406 };
407 let from = from.min(self.values.len()).min(data.len());
408 let request = lower_interval::request(lower);
409 let candles = if self.quote_units {
410 match sources.iter().find(|source| source.request == request) {
412 Some(source)
413 if source.timeframe != lower
414 || (from == 0
415 && source
416 .bars
417 .windows(2)
418 .any(|pair| pair[0].time >= pair[1].time)) =>
419 {
420 None
421 }
422 Some(source) => Some(source.bars),
423 None => Some(&[][..]),
424 }
425 } else {
426 lower_interval::reply(&request, from, sources)
427 };
428 match candles {
429 None if self.quote_units => {
430 self.clear();
431 self.values = IndicatorValues::missing(data.len());
432 self.update_table();
433 }
434 None => {}
435 Some([]) => self.update(data, from),
436 Some(candles) => self.sum_candles(data, from, lower, candles),
437 }
438 }
439
440 fn is_incremental(&self) -> bool {
441 true
442 }
443
444 fn values(&self) -> &IndicatorValues {
445 &self.values
446 }
447
448 fn colors(&self) -> Vec<Color32> {
449 vec![self.color]
450 }
451
452 fn set_colors(&mut self, colors: Vec<Color32>) {
453 if let Some(&color) = colors.first() {
454 self.color = color;
455 }
456 }
457
458 fn is_overlay(&self) -> bool {
459 false
460 }
461
462 fn is_visible(&self) -> bool {
463 self.visible
464 }
465
466 fn set_visible(&mut self, visible: bool) {
467 self.visible = visible;
468 }
469
470 fn clone_box(&self) -> Box<dyn Indicator> {
471 Box::new(self.clone())
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use chrono::TimeZone;
479
480 #[test]
483 fn units_use_each_candle_close_and_recover_after_expiry() {
484 use crate::model::Instrument;
485 use crate::studies::{InputValue, StudyCell};
486 let bar = |h: i64, volume, close| {
487 Bar::new(
488 Utc.timestamp_opt(h * 3_600, 0).unwrap(),
489 close,
490 close,
491 close,
492 close,
493 volume,
494 )
495 };
496 let five_hours = Timeframe::Custom(std::num::NonZeroU64::new(18_000).unwrap());
497 for (interval, data, candles, supplied, quote) in [
500 (
501 Timeframe::Hour1,
502 vec![
503 bar(0, 2.0, 10.0),
504 bar(1, 3.0, 20.0),
505 bar(24, 4.0, 30.0),
506 bar(25, 1.0, 40.0),
507 ],
508 vec![],
509 vec![Some(2.0), Some(5.0), Some(7.0), Some(5.0)],
510 vec![Some(20.0), Some(80.0), Some(180.0), Some(160.0)],
511 ),
512 (
513 five_hours,
514 vec![bar(0, 99.0, 100.0), bar(24, 99.0, 100.0)],
515 vec![
516 bar(0, 2.0, 10.0),
517 bar(1, 3.0, 20.0),
518 bar(24, 4.0, 30.0),
519 bar(25, 1.0, 40.0),
520 ],
521 vec![Some(5.0), Some(5.0)],
522 vec![Some(80.0), Some(160.0)],
523 ),
524 (
525 Timeframe::Week1,
526 vec![bar(4 * 24, 99.0, 100.0), bar(11 * 24, 99.0, 100.0)],
527 vec![
528 bar(7 * 24 + 15, 7.0, 5.0),
529 bar(7 * 24 + 16, 2.0, 10.0),
530 bar(8 * 24 + 15, 3.0, 20.0),
531 bar(17 * 24, 4.0, 30.0),
532 bar(17 * 24 + 23, 1.0, 40.0),
533 ],
534 vec![Some(5.0), Some(5.0)],
535 vec![Some(80.0), Some(160.0)],
536 ),
537 ] {
538 for (units, expected) in [(0, supplied), (1, quote)] {
539 let mut study = Volume24h::new();
540 study.set_instrument(Some(&Instrument::new("TEST").with_currency("USD")));
541 study.set_bar_clock(&BarClock::new(interval));
542 study.set_input("units", InputValue::Choice(units)).unwrap();
543 let lower = study.lower_timeframe().unwrap_or(Timeframe::Hour1);
544 let source = StudyData {
545 request: lower_interval::request(lower),
546 bars: &candles,
547 clock: BarClock::new(lower),
548 instrument: None,
549 timeframe: lower,
550 };
551 for end in 1..=data.len() {
552 study.update_data(&data[..end], end - 1, std::slice::from_ref(&source));
553 assert_eq!(
554 study.values().value(0, end - 1),
555 expected[end - 1],
556 "{interval:?}, units {units}, bar {}",
557 end - 1
558 );
559 }
560 study.update_data(&data, 0, &[source]);
561 assert_eq!(
562 (0..data.len())
563 .map(|i| study.values().value(0, i))
564 .collect::<Vec<_>>(),
565 expected
566 );
567 }
568 }
569 for (volume, close) in [
570 (2.0, f64::NAN),
571 (2.0, f64::INFINITY),
572 (f64::NAN, 10.0),
573 (-2.0, 10.0),
574 (1e308, 10.0),
575 ] {
576 for interval in [Timeframe::Hour1, five_hours, Timeframe::Week1] {
577 let mut study = Volume24h::new();
578 study.set_input("units", InputValue::Choice(1)).unwrap();
579 study.set_instrument(Some(&Instrument::new("TEST").with_currency("USD")));
580 study.set_bar_clock(&BarClock::new(interval));
581 let (data, candles, expected) = if interval == Timeframe::Hour1 {
582 (
583 vec![bar(0, volume, close), bar(1, 3.0, 20.0), bar(24, 4.0, 30.0)],
584 vec![],
585 vec![None, None, Some(180.0)],
586 )
587 } else if interval == five_hours {
588 (
589 vec![bar(0, 99.0, 100.0), bar(24, 99.0, 100.0)],
590 vec![bar(0, volume, close), bar(1, 3.0, 20.0), bar(24, 4.0, 30.0)],
591 vec![None, Some(180.0)],
592 )
593 } else {
594 (
595 vec![bar(4 * 24, 99.0, 100.0), bar(11 * 24, 99.0, 100.0)],
596 vec![
597 bar(7 * 24 + 16, volume, close),
598 bar(8 * 24 + 15, 3.0, 20.0),
599 bar(17 * 24 + 23, 4.0, 30.0),
600 ],
601 vec![None, Some(120.0)],
602 )
603 };
604 let lower = study.lower_timeframe().unwrap_or(Timeframe::Hour1);
605 let source = StudyData {
606 request: lower_interval::request(lower),
607 bars: &candles,
608 clock: BarClock::new(lower),
609 instrument: None,
610 timeframe: lower,
611 };
612 study.update_data(&data, 0, &[source]);
613 assert_eq!(
614 (0..data.len())
615 .map(|i| study.values().value(0, i))
616 .collect::<Vec<_>>(),
617 expected,
618 "{interval:?}, {volume} × {close}"
619 );
620 study.set_instrument(None);
621 study.update_data(&data, 0, &[]);
622 assert!((0..data.len()).all(|i| study.values().value(0, i).is_none()));
623 assert_eq!(
624 study.table().unwrap().rows[0][0],
625 StudyCell::Text("Quote currency unavailable".into())
626 );
627 }
628 }
629 }
630
631 #[cfg(feature = "ui")]
633 #[test]
634 fn units_survive_templates_and_layouts_with_labelled_readouts() {
635 use crate::model::{BarData, Instrument};
636 use crate::studies::{InputValue, StudyTemplate};
637 use crate::widget::{Chart, ChartLayoutMode, SavedLayout};
638 let data = [0_i64, 1].map(|h| {
639 let (volume, close) = if h == 0 { (2.0, 10.0) } else { (3.0, 20.0) };
640 Bar::new(
641 Utc.timestamp_opt(h * 3_600, 0).unwrap(),
642 close,
643 close,
644 close,
645 close,
646 volume,
647 )
648 });
649 for (units, label, value, formatted) in [
650 (0, "Volume (supplied units)", 5.0, "5"),
651 (1, "Amount (USD)", 80.0, "80.00 USD"),
652 ] {
653 let mut study = Volume24h::new();
654 study.set_input("units", InputValue::Choice(units)).unwrap();
655 let json = serde_json::to_string(&StudyTemplate::from_study(&study)).unwrap();
656 let restored = serde_json::from_str::<StudyTemplate>(&json)
657 .unwrap()
658 .create()
659 .unwrap();
660 let mut chart = Chart::new(BarData::from_bars(data.to_vec()));
661 chart.set_timeframe(Timeframe::Hour1);
662 chart.set_instrument(Some(Instrument::new("TEST").with_currency("USD")));
663 let id = chart.add_indicator(restored);
664 let study = chart
665 .indicators()
666 .find(|(candidate, _)| *candidate == id)
667 .unwrap()
668 .1;
669 assert_eq!(study.values().value(0, 1), Some(value));
670 assert!(study.table().is_none());
671 assert_eq!(study.line_names(), [label]);
672 assert_eq!(study.format_value(value), formatted);
673 let table = chart.data_table();
674 assert_eq!(
675 table.headers.last().unwrap(),
676 &format!("24-hour Volume: {label}")
677 );
678 assert_eq!(
679 table.rows[1].last().unwrap(),
680 if units == 0 { "5.00" } else { "80.00" }
681 );
682 let saved = SavedLayout {
683 mode: ChartLayoutMode::Single,
684 charts: vec![chart.snapshot().unwrap()],
685 };
686 let saved = SavedLayout::from_json(&saved.to_json().unwrap()).unwrap();
687 let mut restored = Chart::new(BarData::from_bars(data.to_vec()));
688 restored.set_instrument(Some(Instrument::new("TEST").with_currency("USD")));
689 restored.restore(&saved.charts[0]).unwrap();
690 let study = restored.indicators().next().unwrap().1;
691 assert_eq!(
692 study
693 .inputs()
694 .iter()
695 .find(|input| input.id == "units")
696 .unwrap()
697 .value,
698 InputValue::Choice(units)
699 );
700 assert_eq!(restored.data_table().rows[1].last(), table.rows[1].last());
701 let id = restored.indicators().next().unwrap().0;
702 let study = restored.indicator_mut(id).unwrap();
703 let mut settings = study.settings();
704 settings.precision = Some(1);
705 study.apply_settings(&[], settings).unwrap();
706 assert_eq!(
707 study.format_value(value),
708 if units == 0 { "5.0" } else { "80.0 USD" }
709 );
710 assert_eq!(
711 restored.data_table().rows[1].last().unwrap(),
712 if units == 0 { "5.0" } else { "80.0" }
713 );
714 let study = restored.indicator_mut(id).unwrap();
715 study.set_input("units", InputValue::Choice(0)).unwrap();
716 study.set_value_formatter(Some(std::sync::Arc::new(crate::scales::VolumeFormatter {
717 precision: 3,
718 })));
719 if units == 0 {
720 study.set_input("units", InputValue::Choice(1)).unwrap();
721 } else {
722 let settings = study.settings();
723 study
724 .apply_settings(&[("units".into(), InputValue::Choice(1))], settings)
725 .unwrap();
726 }
727 assert_eq!(study.format_value(80.0), "80.0 USD");
728 }
729 }
730
731 #[test]
734 fn a_dominant_volume_leaves_no_residue() {
735 let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
736 let bars: Vec<Bar> = (0..60)
737 .map(|h| {
738 let volume = if h == 0 { 1e17 } else { 1.0 };
739 Bar::new(start + TimeDelta::hours(h), 1.0, 1.0, 1.0, 1.0, volume)
740 })
741 .collect();
742 let mut study = Volume24h::new();
743 study.calculate(&bars);
744 for bar in 24..60 {
745 assert_eq!(study.values().value(0, bar), Some(24.0), "bar {bar}");
746 }
747 }
748
749 #[test]
753 fn long_and_uneven_intervals_sum_lower_interval_candles() {
754 use crate::studies::{StudyData, StudyDataRequest};
755 let (hour, day) = (3_600, 86_400);
756 let bar =
757 |s: i64, volume| Bar::new(Utc.timestamp_opt(s, 0).unwrap(), 1.0, 1.0, 1.0, 1.0, volume);
758 let five_hours = Timeframe::Custom(std::num::NonZeroU64::new(18_000).unwrap());
759 let mut study = Volume24h::new();
760 for (chart, lower) in [
761 (Timeframe::Hour1, None),
762 (Timeframe::Day1, None),
763 (five_hours, Some(Timeframe::Min1)),
764 (Timeframe::Week1, Some(Timeframe::Hour1)),
765 ] {
766 study.set_bar_clock(&BarClock::new(chart));
767 let requested = study.data_requests().first().and_then(|r| r.timeframe);
768 assert_eq!(requested, lower, "{chart:?}");
769 }
770 let sums = |study: &mut Volume24h, data: &[Bar], lower: Timeframe, candles: &[Bar]| {
771 let source = StudyData {
772 request: StudyDataRequest {
773 metric: None,
774 symbol: String::new(),
775 timeframe: Some(lower),
776 },
777 bars: candles,
778 clock: BarClock::new(lower),
779 instrument: None,
780 timeframe: lower,
781 };
782 study.update_data(data, 0, &[source]);
783 (0..data.len())
784 .map(|i| study.values().value(0, i))
785 .collect::<Vec<_>>()
786 };
787 let weeks = [4, 11, 18].map(|d| bar(d * day, 1.0));
791 let hourly = [
792 bar(7 * day + 15 * hour, 1.0), bar(7 * day + 16 * hour, 2.0), bar(8 * day + 15 * hour, 4.0), bar(16 * day + 23 * hour, 8.0), bar(17 * day, 16.0), bar(17 * day + 23 * hour, 32.0), ];
799 assert_eq!(
800 sums(&mut study, &weeks, Timeframe::Hour1, &hourly),
801 [Some(6.0), Some(48.0), None]
802 );
803 study.set_bar_clock(&BarClock::new(five_hours));
806 let bars = [0, 5, 10, 15, 20, 24, 29].map(|h| bar(h * hour, 1.0));
807 let minutes = [
808 (17_940, 1.0),
809 (18_000, 2.0),
810 (35_940, 4.0),
811 (104_340, 8.0),
812 (104_400, 16.0),
813 ]
814 .map(|(s, v)| bar(s, v));
815 assert_eq!(
816 sums(&mut study, &bars, Timeframe::Min1, &minutes),
817 [
818 Some(1.0),
819 Some(7.0),
820 None,
821 None,
822 None,
823 Some(14.0),
824 Some(28.0)
825 ]
826 );
827 assert_eq!(
829 sums(&mut study, &bars, Timeframe::Min1, &[]),
830 [1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0].map(Some)
831 );
832 }
833}