1use std::{fmt, marker::PhantomData};
2
3use paft::domain::AssetKind;
4use serde::Serialize;
5use serde_json::{Value, json};
6
7use crate::{YfError, core::yahoo_vocab::parse_yahoo_quote_type};
8
9#[derive(Debug, Clone, Copy)]
11pub struct Predefined;
12
13#[derive(Debug, Clone, Copy)]
15pub struct Equity;
16
17#[derive(Debug, Clone, Copy)]
19pub struct Fund;
20
21#[derive(Debug, Clone, Copy)]
23pub struct Etf;
24
25pub type EquityQuery = ScreenerQuery<Equity>;
27
28pub type FundQuery = ScreenerQuery<Fund>;
30
31pub type EtfQuery = ScreenerQuery<Etf>;
33
34#[non_exhaustive]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
37pub enum YahooQuoteType {
38 #[serde(rename = "EQUITY")]
40 Equity,
41 #[serde(rename = "MUTUALFUND")]
43 MutualFund,
44 #[serde(rename = "ETF")]
46 Etf,
47}
48
49impl YahooQuoteType {
50 pub(crate) const fn as_str(self) -> &'static str {
51 match self {
52 Self::Equity => "EQUITY",
53 Self::MutualFund => "MUTUALFUND",
54 Self::Etf => "ETF",
55 }
56 }
57
58 pub(crate) fn asset_kind(self) -> AssetKind {
59 parse_yahoo_quote_type(self.as_str()).expect("typed screener quote type is valid")
60 }
61
62 pub(crate) fn parse(value: &str) -> Option<Self> {
63 match value {
64 "EQUITY" => Some(Self::Equity),
65 "MUTUALFUND" => Some(Self::MutualFund),
66 "ETF" => Some(Self::Etf),
67 _ => None,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74pub struct ScreenerCount(u32);
75
76impl ScreenerCount {
77 pub const MAX: u32 = 250;
79
80 pub(crate) const DEFAULT: Self = Self(25);
81
82 pub fn new(value: u32) -> Result<Self, YfError> {
89 if value == 0 || value > Self::MAX {
90 return Err(YfError::InvalidParams(format!(
91 "screener count must be between 1 and {}",
92 Self::MAX
93 )));
94 }
95 Ok(Self(value))
96 }
97
98 pub(crate) const fn get(self) -> u32 {
99 self.0
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
105pub struct ResultOffset(u32);
106
107impl ResultOffset {
108 pub(crate) const ZERO: Self = Self(0);
109
110 #[must_use]
112 pub const fn new(value: u32) -> Self {
113 Self(value)
114 }
115
116 pub fn try_from_i64(value: i64) -> Result<Self, YfError> {
123 let value = u32::try_from(value)
124 .map_err(|_| YfError::InvalidParams("screener offset must be nonnegative".into()))?;
125 Ok(Self(value))
126 }
127
128 pub(crate) const fn get(self) -> u32 {
129 self.0
130 }
131}
132
133#[derive(Clone, Copy, PartialEq)]
138pub struct ScreenerNumber(ScreenerNumberKind);
139
140#[derive(Debug, Clone, Copy, PartialEq)]
141enum ScreenerNumberKind {
142 Float(FiniteF64),
143 Unsigned(u64),
144 Signed(i64),
145}
146
147#[derive(Debug, Clone, Copy, PartialEq)]
148struct FiniteF64(f64);
149
150impl FiniteF64 {
151 fn new(value: f64) -> Result<Self, YfError> {
152 if !value.is_finite() {
153 return Err(YfError::InvalidParams(
154 "screener numeric value must be finite".into(),
155 ));
156 }
157
158 Ok(Self(value))
159 }
160
161 fn into_json_number(self) -> serde_json::Number {
162 serde_json::Number::from_f64(self.0).expect("FiniteF64 stores finite values")
163 }
164}
165
166impl ScreenerNumber {
167 pub fn new(value: f64) -> Result<Self, YfError> {
173 Ok(Self(ScreenerNumberKind::Float(FiniteF64::new(value)?)))
174 }
175
176 fn to_value(self) -> Value {
177 match self.0 {
178 ScreenerNumberKind::Float(value) => Value::Number(value.into_json_number()),
179 ScreenerNumberKind::Unsigned(value) => Value::Number(serde_json::Number::from(value)),
180 ScreenerNumberKind::Signed(value) => Value::Number(serde_json::Number::from(value)),
181 }
182 }
183}
184
185impl fmt::Debug for ScreenerNumber {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self.0 {
188 ScreenerNumberKind::Float(value) => f.debug_tuple("Float").field(&value.0).finish(),
189 ScreenerNumberKind::Unsigned(value) => f.debug_tuple("Unsigned").field(&value).finish(),
190 ScreenerNumberKind::Signed(value) => f.debug_tuple("Signed").field(&value).finish(),
191 }
192 }
193}
194
195impl From<u32> for ScreenerNumber {
196 fn from(value: u32) -> Self {
197 Self(ScreenerNumberKind::Unsigned(u64::from(value)))
198 }
199}
200
201impl From<i32> for ScreenerNumber {
202 fn from(value: i32) -> Self {
203 Self(ScreenerNumberKind::Signed(i64::from(value)))
204 }
205}
206
207impl From<u64> for ScreenerNumber {
208 fn from(value: u64) -> Self {
209 Self(ScreenerNumberKind::Unsigned(value))
210 }
211}
212
213impl From<i64> for ScreenerNumber {
214 fn from(value: i64) -> Self {
215 Self(ScreenerNumberKind::Signed(value))
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq)]
223pub struct PercentPoints(ScreenerNumber);
224
225impl PercentPoints {
226 pub fn new(value: f64) -> Result<Self, YfError> {
232 Ok(Self(ScreenerNumber::new(value)?))
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
238pub enum SortDirection {
239 Asc,
241 Desc,
243}
244
245impl SortDirection {
246 pub(crate) const fn as_str(self) -> &'static str {
247 match self {
248 Self::Asc => "ASC",
249 Self::Desc => "DESC",
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
256pub enum Region {
257 Us,
259}
260
261impl Region {
262 const fn as_query_value(self) -> &'static str {
263 match self {
264 Self::Us => "us",
265 }
266 }
267}
268
269#[non_exhaustive]
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
272pub enum YahooExchangeCode {
273 #[serde(rename = "NMS")]
275 Nms,
276 #[serde(rename = "NGM")]
278 Ngm,
279 #[serde(rename = "NCM")]
281 Ncm,
282 #[serde(rename = "NYQ")]
284 Nyq,
285 #[serde(rename = "NAS")]
287 Nas,
288 #[serde(rename = "ASE")]
290 Ase,
291 #[serde(rename = "BTS")]
293 Bts,
294 #[serde(rename = "PCX")]
296 Pcx,
297 #[serde(rename = "PNK")]
299 Pnk,
300 #[serde(rename = "OQB")]
302 Oqb,
303 #[serde(rename = "OQX")]
305 Oqx,
306 #[serde(rename = "OEM")]
308 Oem,
309 #[serde(rename = "YHD")]
311 Yhd,
312 #[serde(rename = "CXI")]
314 Cxi,
315 #[serde(rename = "NAE")]
317 Nae,
318 #[serde(rename = "OGM")]
320 Ogm,
321 #[serde(rename = "WCB")]
323 Wcb,
324}
325
326impl YahooExchangeCode {
327 #[must_use]
329 pub const fn code(self) -> &'static str {
330 match self {
331 Self::Nms => "NMS",
332 Self::Ngm => "NGM",
333 Self::Ncm => "NCM",
334 Self::Nyq => "NYQ",
335 Self::Nas => "NAS",
336 Self::Ase => "ASE",
337 Self::Bts => "BTS",
338 Self::Pcx => "PCX",
339 Self::Pnk => "PNK",
340 Self::Oqb => "OQB",
341 Self::Oqx => "OQX",
342 Self::Oem => "OEM",
343 Self::Yhd => "YHD",
344 Self::Cxi => "CXI",
345 Self::Nae => "NAE",
346 Self::Ogm => "OGM",
347 Self::Wcb => "WCB",
348 }
349 }
350
351 pub(crate) fn parse(value: &str) -> Option<Self> {
352 match value {
353 "NMS" => Some(Self::Nms),
354 "NGM" => Some(Self::Ngm),
355 "NCM" => Some(Self::Ncm),
356 "NYQ" => Some(Self::Nyq),
357 "NAS" => Some(Self::Nas),
358 "ASE" => Some(Self::Ase),
359 "BTS" => Some(Self::Bts),
360 "PCX" => Some(Self::Pcx),
361 "PNK" => Some(Self::Pnk),
362 "OQB" => Some(Self::Oqb),
363 "OQX" => Some(Self::Oqx),
364 "OEM" => Some(Self::Oem),
365 "YHD" => Some(Self::Yhd),
366 "CXI" => Some(Self::Cxi),
367 "NAE" => Some(Self::Nae),
368 "OGM" => Some(Self::Ogm),
369 "WCB" => Some(Self::Wcb),
370 _ => None,
371 }
372 }
373}
374
375#[non_exhaustive]
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
378pub enum EquitySector {
379 BasicMaterials,
381 Industrials,
383 CommunicationServices,
385 Healthcare,
387 RealEstate,
389 Technology,
391 Energy,
393 Utilities,
395 FinancialServices,
397 ConsumerDefensive,
399 ConsumerCyclical,
401}
402
403impl EquitySector {
404 const fn as_str(self) -> &'static str {
405 match self {
406 Self::BasicMaterials => "Basic Materials",
407 Self::Industrials => "Industrials",
408 Self::CommunicationServices => "Communication Services",
409 Self::Healthcare => "Healthcare",
410 Self::RealEstate => "Real Estate",
411 Self::Technology => "Technology",
412 Self::Energy => "Energy",
413 Self::Utilities => "Utilities",
414 Self::FinancialServices => "Financial Services",
415 Self::ConsumerDefensive => "Consumer Defensive",
416 Self::ConsumerCyclical => "Consumer Cyclical",
417 }
418 }
419}
420
421#[non_exhaustive]
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
424pub enum FundCategory {
425 ForeignLargeValue,
427 ForeignLargeBlend,
429 ForeignLargeGrowth,
431 ForeignSmallMidGrowth,
433 ForeignSmallMidBlend,
435 ForeignSmallMidValue,
437 HighYieldBond,
439 LargeBlend,
441 LargeGrowth,
443 MidCapGrowth,
445}
446
447impl FundCategory {
448 const fn as_str(self) -> &'static str {
449 match self {
450 Self::ForeignLargeValue => "Foreign Large Value",
451 Self::ForeignLargeBlend => "Foreign Large Blend",
452 Self::ForeignLargeGrowth => "Foreign Large Growth",
453 Self::ForeignSmallMidGrowth => "Foreign Small/Mid Growth",
454 Self::ForeignSmallMidBlend => "Foreign Small/Mid Blend",
455 Self::ForeignSmallMidValue => "Foreign Small/Mid Value",
456 Self::HighYieldBond => "High Yield Bond",
457 Self::LargeBlend => "Large Blend",
458 Self::LargeGrowth => "Large Growth",
459 Self::MidCapGrowth => "Mid-Cap Growth",
460 }
461 }
462}
463
464#[non_exhaustive]
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
467pub enum EtfCategory {
468 Technology,
470 CorporateBond,
472 EmergingMarketsBond,
474 EmergingMarketsLocalCurrencyBond,
476 HighYieldBond,
478 IntermediateTermBond,
480 LongTermBond,
482 InflationProtectedBond,
484 MultisectorBond,
486 NontraditionalBond,
488 ShortTermBond,
490 UltrashortBond,
492 WorldBond,
494}
495
496impl EtfCategory {
497 const fn as_str(self) -> &'static str {
498 match self {
499 Self::Technology => "Technology",
500 Self::CorporateBond => "Corporate Bond",
501 Self::EmergingMarketsBond => "Emerging Markets Bond",
502 Self::EmergingMarketsLocalCurrencyBond => "Emerging-Markets Local-Currency Bond",
503 Self::HighYieldBond => "High Yield Bond",
504 Self::IntermediateTermBond => "Intermediate-Term Bond",
505 Self::LongTermBond => "Long-Term Bond",
506 Self::InflationProtectedBond => "Inflation-Protected Bond",
507 Self::MultisectorBond => "Multisector Bond",
508 Self::NontraditionalBond => "Nontraditional Bond",
509 Self::ShortTermBond => "Short-Term Bond",
510 Self::UltrashortBond => "Ultrashort Bond",
511 Self::WorldBond => "World Bond",
512 }
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
518pub enum Rating {
519 One,
521 Two,
523 Three,
525 Four,
527 Five,
529}
530
531impl Rating {
532 const fn value(self) -> u8 {
533 match self {
534 Self::One => 1,
535 Self::Two => 2,
536 Self::Three => 3,
537 Self::Four => 4,
538 Self::Five => 5,
539 }
540 }
541}
542
543mod sealed {
544 pub trait Sealed {}
545}
546
547pub trait ScreenerValue: Copy + sealed::Sealed {
549 #[doc(hidden)]
551 fn to_wire_value(self) -> Value;
552}
553
554macro_rules! impl_screener_value {
555 ($type:ty, $body:expr) => {
556 impl sealed::Sealed for $type {}
557
558 impl ScreenerValue for $type {
559 fn to_wire_value(self) -> Value {
560 $body(self)
561 }
562 }
563 };
564}
565
566impl_screener_value!(ScreenerNumber, ScreenerNumber::to_value);
567impl_screener_value!(PercentPoints, |value: PercentPoints| value.0.to_value());
568impl_screener_value!(Region, |value: Region| {
569 Value::String(value.as_query_value().to_string())
570});
571impl_screener_value!(YahooExchangeCode, |value: YahooExchangeCode| {
572 Value::String(value.code().to_string())
573});
574impl_screener_value!(EquitySector, |value: EquitySector| {
575 Value::String(value.as_str().to_string())
576});
577impl_screener_value!(FundCategory, |value: FundCategory| {
578 Value::String(value.as_str().to_string())
579});
580impl_screener_value!(EtfCategory, |value: EtfCategory| {
581 Value::String(value.as_str().to_string())
582});
583impl_screener_value!(Rating, |value: Rating| {
584 Value::Number(serde_json::Number::from(value.value()))
585});
586
587#[derive(Debug, Clone, Copy)]
589pub struct NumericField<U> {
590 key: &'static str,
591 marker: PhantomData<U>,
592}
593
594impl<U> NumericField<U> {
595 pub(crate) const fn new(key: &'static str) -> Self {
596 Self {
597 key,
598 marker: PhantomData,
599 }
600 }
601
602 #[must_use]
604 pub fn eq<N: Into<ScreenerNumber>>(self, value: N) -> ScreenerQuery<U> {
605 ScreenerQuery::comparison("EQ", self.key, value.into().to_value())
606 }
607
608 pub fn one_of<I, N>(self, values: I) -> Result<ScreenerQuery<U>, YfError>
614 where
615 I: IntoIterator<Item = N>,
616 N: Into<ScreenerNumber>,
617 {
618 ScreenerQuery::one_of_values(self.key, values.into_iter().map(|v| v.into().to_value()))
619 }
620
621 #[must_use]
623 pub fn gt<N: Into<ScreenerNumber>>(self, value: N) -> ScreenerQuery<U> {
624 ScreenerQuery::comparison("GT", self.key, value.into().to_value())
625 }
626
627 #[must_use]
629 pub fn gte<N: Into<ScreenerNumber>>(self, value: N) -> ScreenerQuery<U> {
630 ScreenerQuery::comparison("GTE", self.key, value.into().to_value())
631 }
632
633 #[must_use]
635 pub fn lt<N: Into<ScreenerNumber>>(self, value: N) -> ScreenerQuery<U> {
636 ScreenerQuery::comparison("LT", self.key, value.into().to_value())
637 }
638
639 #[must_use]
641 pub fn lte<N: Into<ScreenerNumber>>(self, value: N) -> ScreenerQuery<U> {
642 ScreenerQuery::comparison("LTE", self.key, value.into().to_value())
643 }
644
645 #[must_use]
647 pub fn between<N1, N2>(self, lower: N1, upper: N2) -> ScreenerQuery<U>
648 where
649 N1: Into<ScreenerNumber>,
650 N2: Into<ScreenerNumber>,
651 {
652 ScreenerQuery::comparison(
653 "BTWN",
654 self.key,
655 json!([lower.into().to_value(), upper.into().to_value()]),
656 )
657 }
658}
659
660#[derive(Debug, Clone, Copy)]
662pub struct PercentField<U> {
663 key: &'static str,
664 marker: PhantomData<U>,
665}
666
667impl<U> PercentField<U> {
668 pub(crate) const fn new(key: &'static str) -> Self {
669 Self {
670 key,
671 marker: PhantomData,
672 }
673 }
674
675 #[must_use]
677 pub fn gt(self, value: PercentPoints) -> ScreenerQuery<U> {
678 ScreenerQuery::comparison("GT", self.key, value.to_wire_value())
679 }
680
681 #[must_use]
683 pub fn gte(self, value: PercentPoints) -> ScreenerQuery<U> {
684 ScreenerQuery::comparison("GTE", self.key, value.to_wire_value())
685 }
686
687 #[must_use]
689 pub fn lt(self, value: PercentPoints) -> ScreenerQuery<U> {
690 ScreenerQuery::comparison("LT", self.key, value.to_wire_value())
691 }
692
693 #[must_use]
695 pub fn lte(self, value: PercentPoints) -> ScreenerQuery<U> {
696 ScreenerQuery::comparison("LTE", self.key, value.to_wire_value())
697 }
698}
699
700#[derive(Debug, Clone, Copy)]
702pub struct EnumField<U, V> {
703 key: &'static str,
704 marker: PhantomData<(U, V)>,
705}
706
707impl<U, V> EnumField<U, V>
708where
709 V: ScreenerValue,
710{
711 pub(crate) const fn new(key: &'static str) -> Self {
712 Self {
713 key,
714 marker: PhantomData,
715 }
716 }
717
718 #[must_use]
720 pub fn eq(self, value: V) -> ScreenerQuery<U> {
721 ScreenerQuery::comparison("EQ", self.key, value.to_wire_value())
722 }
723
724 pub fn one_of<I>(self, values: I) -> Result<ScreenerQuery<U>, YfError>
730 where
731 I: IntoIterator<Item = V>,
732 {
733 ScreenerQuery::one_of_values(
734 self.key,
735 values.into_iter().map(ScreenerValue::to_wire_value),
736 )
737 }
738}
739
740#[derive(Debug, Clone, Copy)]
742pub struct SortField<U> {
743 key: &'static str,
744 marker: PhantomData<U>,
745}
746
747impl<U> SortField<U> {
748 pub(crate) const fn new(key: &'static str) -> Self {
749 Self {
750 key,
751 marker: PhantomData,
752 }
753 }
754
755 pub(crate) const fn key(self) -> &'static str {
756 self.key
757 }
758}
759
760#[derive(Debug, Clone)]
761enum QueryNode {
762 Comparison {
763 operator: &'static str,
764 field: &'static str,
765 values: Vec<Value>,
766 },
767 Logical {
768 operator: &'static str,
769 operands: Vec<Self>,
770 },
771}
772
773impl QueryNode {
774 fn to_wire_value(&self) -> Value {
775 match self {
776 Self::Comparison {
777 operator,
778 field,
779 values,
780 } => {
781 let mut operands = Vec::with_capacity(values.len() + 1);
782 operands.push(Value::String((*field).to_string()));
783 operands.extend(values.iter().cloned());
784 json!({
785 "operator": *operator,
786 "operands": operands,
787 })
788 }
789 Self::Logical { operator, operands } => json!({
790 "operator": *operator,
791 "operands": operands.iter().map(Self::to_wire_value).collect::<Vec<_>>(),
792 }),
793 }
794 }
795}
796
797#[derive(Debug, Clone)]
799pub struct ScreenerQuery<U> {
800 node: QueryNode,
801 marker: PhantomData<U>,
802}
803
804impl<U> ScreenerQuery<U> {
805 fn comparison(operator: &'static str, field: &'static str, value: Value) -> Self {
806 let values = match (operator, value) {
807 ("BTWN", Value::Array(values)) => values,
808 (_, value) => vec![value],
809 };
810
811 Self {
812 node: QueryNode::Comparison {
813 operator,
814 field,
815 values,
816 },
817 marker: PhantomData,
818 }
819 }
820
821 fn one_of_values<I>(field: &'static str, values: I) -> Result<Self, YfError>
822 where
823 I: IntoIterator<Item = Value>,
824 {
825 let operands = values
826 .into_iter()
827 .map(|value| QueryNode::Comparison {
828 operator: "EQ",
829 field,
830 values: vec![value],
831 })
832 .collect::<Vec<_>>();
833
834 if operands.is_empty() {
835 return Err(YfError::InvalidParams(
836 "one_of requires at least one value".into(),
837 ));
838 }
839
840 Ok(Self {
841 node: QueryNode::Logical {
842 operator: "OR",
843 operands,
844 },
845 marker: PhantomData,
846 })
847 }
848
849 pub fn and<I>(queries: I) -> Result<Self, YfError>
856 where
857 I: IntoIterator<Item = Self>,
858 {
859 Self::logical("AND", queries)
860 }
861
862 pub fn or<I>(queries: I) -> Result<Self, YfError>
869 where
870 I: IntoIterator<Item = Self>,
871 {
872 Self::logical("OR", queries)
873 }
874
875 fn logical<I>(operator: &'static str, queries: I) -> Result<Self, YfError>
876 where
877 I: IntoIterator<Item = Self>,
878 {
879 let operands = queries.into_iter().map(|q| q.node).collect::<Vec<_>>();
880
881 if operands.len() <= 1 {
882 return Err(YfError::InvalidParams(format!(
883 "{operator} requires at least two operands"
884 )));
885 }
886
887 Ok(Self {
888 node: QueryNode::Logical { operator, operands },
889 marker: PhantomData,
890 })
891 }
892
893 pub(crate) fn into_wire_value(self) -> Value {
894 self.node.to_wire_value()
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901 use crate::screener::equity_fields;
902
903 #[test]
904 fn one_of_expands_to_or_of_eq_filters() {
905 let query = equity_fields::EXCHANGE
906 .one_of([YahooExchangeCode::Nms, YahooExchangeCode::Nyq])
907 .unwrap();
908
909 assert_eq!(
910 query.into_wire_value(),
911 json!({
912 "operator": "OR",
913 "operands": [
914 {"operator": "EQ", "operands": ["exchange", "NMS"]},
915 {"operator": "EQ", "operands": ["exchange", "NYQ"]}
916 ]
917 })
918 );
919 }
920
921 #[test]
922 fn and_rejects_empty_or_single_operand() {
923 assert!(EquityQuery::and(Vec::<EquityQuery>::new()).is_err());
924 assert!(EquityQuery::and([equity_fields::INTRADAY_PRICE.gt(1)]).is_err());
925 }
926
927 #[test]
928 fn screener_number_serializes_validated_float() {
929 let query = equity_fields::INTRADAY_PRICE.gt(ScreenerNumber::new(12.5).unwrap());
930
931 assert_eq!(
932 query.into_wire_value(),
933 json!({
934 "operator": "GT",
935 "operands": ["intradayprice", 12.5]
936 })
937 );
938 }
939
940 #[test]
941 fn bounded_values_reject_invalid_inputs() {
942 assert!(ScreenerCount::new(0).is_err());
943 assert!(ScreenerCount::new(251).is_err());
944 assert!(ScreenerNumber::new(f64::NAN).is_err());
945 assert!(ScreenerNumber::new(f64::INFINITY).is_err());
946 assert!(ResultOffset::try_from_i64(-1).is_err());
947 }
948}