stock_symbol/
lib.rs

1#![deny(unsafe_op_in_unsafe_fn, rust_2018_idioms)]
2#![warn(missing_docs)]
3#![doc = include_str!("../README.md")]
4
5use std::{
6    cmp::Ordering,
7    error::Error,
8    fmt::{self, Debug, Display, Formatter},
9    hash::{Hash, Hasher},
10    num::NonZeroU64,
11    ops::Deref,
12    ptr, str,
13};
14
15/// A stock symbol, or ticker.
16#[repr(transparent)]
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18pub struct Symbol {
19    data: NonZeroU64,
20}
21
22impl Symbol {
23    /// Converts the given `str` to a `Symbol`, returning an error if the provided string is an
24    /// invalid length.
25    ///
26    /// Symbols must contain 7 or fewer characters, and must not be empty. If the provided string
27    /// does not meet these criteria then [`InvalidSymbol`](crate::InvalidSymbol) is returned
28    /// within the error variant.
29    pub fn from_str<T: AsRef<str>>(symbol: T) -> Result<Self, InvalidSymbol> {
30        #[inline]
31        fn from_str_inner(symbol: &str) -> Result<Symbol, InvalidSymbol> {
32            let len = symbol.len();
33
34            if len.wrapping_sub(1) < 7 {
35                // SAFETY: the check above asserts that the string is non-empty (else the
36                // subtraction would overflow making the value greater than 7), and contains
37                // fewer than eight bytes (since we subtract 1, we check this by ensuring the
38                // altered value is less than 7).
39                Ok(unsafe { Symbol::from_str_unchecked(symbol) })
40            } else {
41                Err(InvalidSymbol)
42            }
43        }
44
45        from_str_inner(symbol.as_ref())
46    }
47
48    /// Converts the given string to a `Symbol` without a length check. This is equivalent to
49    /// `Symbol::from_bytes_unchecked(symbol.as_bytes())`.
50    ///
51    /// # Safety
52    ///
53    /// The given `str` must not be empty, and must contain fewer than eight bytes.
54    #[inline]
55    pub unsafe fn from_str_unchecked(symbol: &str) -> Self {
56        // SAFETY: the length requirement is upheld by the caller, and since the bytes are
57        // obtained from a `str`, they are valid UTF-8.
58        unsafe { Self::from_bytes_unchecked(symbol.as_bytes()) }
59    }
60
61    /// Converts the given slice to a `Symbol` without a length check.
62    ///
63    /// # Safety
64    ///
65    /// The given slice must not be empty, and must contain fewer than eight bytes. Additionally,
66    /// the slice must satisfy the safety conditions of
67    /// [`from_utf8_unchecked`](std::str::from_utf8_unchecked).
68    #[inline]
69    pub unsafe fn from_bytes_unchecked(symbol: &[u8]) -> Self {
70        let mut bytes = [0u8; 8];
71        let len = symbol.len();
72        bytes[7] = len as u8;
73
74        // SAFETY: the caller ensures `symbol` contains fewer than 8 bytes, so `bytes` is valid for
75        // writes of `len` bytes. Moreover, since `len` is the length of `symbol`, `symbol` is
76        // valid for reads of `len` bytes. The source and destination are trivially
77        // non-overlapping, and trivially aligned because they were obtained from valid references.
78        unsafe { ptr::copy_nonoverlapping(symbol.as_ptr(), bytes.as_mut_ptr(), len) };
79
80        Self {
81            // SAFETY: since the caller ensures the lenth is not zero, we know that byte within
82            // the integer is not zero, so the integer overall is non-zero.
83            data: unsafe { NonZeroU64::new_unchecked(u64::from_ne_bytes(bytes)) },
84        }
85    }
86
87    /// Returns a `&str` representing this symbol as a string.
88    ///
89    /// This operation is not a no-op, but is very cheap. The return value will compare equal
90    /// to the string this symbol was constructed with.
91    #[inline]
92    pub fn as_str(&self) -> &str {
93        // SAFETY: [u8; 8] and NonZeroU64 have the same size. [u8; 8] is a POD type, and NonZeroU64
94        // does not contain any uninitialized bytes, so it is safe to cast an immutable reference
95        // from the latter to the former (see bytemuck). Moreover, NonZeroU64 has an alignment of
96        // 8, and [u8; 8] has an alignment of 1, so the created reference is properly aligned.
97        let bytes = unsafe { &*(&self.data as *const NonZeroU64).cast::<[u8; 8]>() };
98
99        let len = usize::from(bytes[7]);
100
101        // SAFETY: since the only way to (safely) construct a value of this type involves ensuring
102        // its length is less than 8, we know the length is in bounds.
103        let str_bytes = unsafe { bytes.get_unchecked(..len) };
104
105        // SAFETY: to (safely) construct a value of this type, the caller must assert that the
106        // bytes forming the symbol contain valid UTF-8, so the safety conditions of this function
107        // are satisfied.
108        unsafe { str::from_utf8_unchecked(str_bytes) }
109    }
110}
111
112impl Debug for Symbol {
113    #[inline]
114    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
115        write!(f, "Symbol({})", self.as_str())
116    }
117}
118
119impl Display for Symbol {
120    #[inline]
121    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
122        f.pad(self.as_str())
123    }
124}
125
126impl AsRef<str> for Symbol {
127    #[inline]
128    fn as_ref(&self) -> &str {
129        self.as_str()
130    }
131}
132
133impl Deref for Symbol {
134    type Target = str;
135
136    #[inline]
137    fn deref(&self) -> &Self::Target {
138        self.as_str()
139    }
140}
141
142impl PartialEq<str> for Symbol {
143    #[inline]
144    fn eq(&self, other: &str) -> bool {
145        self.as_str() == other
146    }
147}
148
149impl PartialEq<Symbol> for str {
150    #[inline]
151    fn eq(&self, other: &Symbol) -> bool {
152        other == self
153    }
154}
155
156impl PartialEq<&str> for Symbol {
157    #[inline]
158    fn eq(&self, other: &&str) -> bool {
159        self == *other
160    }
161}
162
163impl PartialEq<Symbol> for &str {
164    #[inline]
165    fn eq(&self, other: &Symbol) -> bool {
166        *self == other
167    }
168}
169
170impl PartialEq<String> for Symbol {
171    #[inline]
172    fn eq(&self, other: &String) -> bool {
173        self == &**other
174    }
175}
176
177impl PartialEq<Symbol> for String {
178    #[inline]
179    fn eq(&self, other: &Symbol) -> bool {
180        self.as_str() == other
181    }
182}
183
184impl PartialOrd for Symbol {
185    #[inline]
186    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
187        let this = u64::from_be(self.data.get());
188        let other = u64::from_be(other.data.get());
189        this.partial_cmp(&other)
190    }
191}
192
193impl Ord for Symbol {
194    #[inline]
195    fn cmp(&self, other: &Self) -> Ordering {
196        let this = u64::from_be(self.data.get());
197        let other = u64::from_be(other.data.get());
198        this.cmp(&other)
199    }
200}
201
202/// An error signifying an invalid symbol was encountered when parsing. See
203/// `Symbol::`[`from_str`](crate::Symbol::from_str) for details.
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub struct InvalidSymbol;
206
207impl Display for InvalidSymbol {
208    #[inline]
209    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
210        f.write_str("Invalid symbol length; length must be greater than 0 and less than 8")
211    }
212}
213
214impl Error for InvalidSymbol {}
215
216/// A stock symbol, or ticker. Unlike `Symbol`, this type can represent strings of any length,
217/// including the empty string.
218#[derive(Clone)]
219pub enum LongSymbol {
220    /// A compact representation of the symbol, stored inline.
221    Inline(Symbol),
222    /// A general representation of the symbol, stored on the heap.
223    Heap(Box<str>),
224}
225
226impl LongSymbol {
227    /// Attempt to convert this long symbol into a regular `Symbol`. If this conversion fails, then
228    /// `None` is returned.
229    #[inline]
230    pub fn to_symbol(&self) -> Option<Symbol> {
231        match self {
232            Self::Inline(symbol) => Some(*symbol),
233            Self::Heap(string) => Symbol::from_str(&**string).ok(),
234        }
235    }
236
237    /// Returns a `&str` representing this symbol as a string.
238    #[inline]
239    pub fn as_str(&self) -> &str {
240        match self {
241            Self::Inline(symbol) => symbol.as_str(),
242            Self::Heap(string) => &**string,
243        }
244    }
245}
246
247impl From<Symbol> for LongSymbol {
248    #[inline]
249    fn from(value: Symbol) -> Self {
250        Self::Inline(value)
251    }
252}
253
254impl From<String> for LongSymbol {
255    #[inline]
256    fn from(value: String) -> Self {
257        match Symbol::from_str(value.as_str()) {
258            Ok(symbol) => Self::Inline(symbol),
259            Err(_) => Self::Heap(value.into_boxed_str()),
260        }
261    }
262}
263
264impl From<Box<str>> for LongSymbol {
265    #[inline]
266    fn from(value: Box<str>) -> Self {
267        match Symbol::from_str(&*value) {
268            Ok(symbol) => Self::Inline(symbol),
269            Err(_) => Self::Heap(value),
270        }
271    }
272}
273
274impl From<&str> for LongSymbol {
275    #[inline]
276    fn from(value: &str) -> Self {
277        match Symbol::from_str(value) {
278            Ok(symbol) => Self::Inline(symbol),
279            Err(_) => Self::Heap(Box::from(value)),
280        }
281    }
282}
283
284impl From<LongSymbol> for String {
285    #[inline]
286    fn from(value: LongSymbol) -> Self {
287        match value {
288            LongSymbol::Inline(symbol) => symbol.as_str().to_owned(),
289            LongSymbol::Heap(string) => String::from(string),
290        }
291    }
292}
293
294impl From<LongSymbol> for Box<str> {
295    #[inline]
296    fn from(value: LongSymbol) -> Self {
297        match value {
298            LongSymbol::Inline(symbol) => Box::from(symbol.as_str()),
299            LongSymbol::Heap(string) => string,
300        }
301    }
302}
303
304impl Debug for LongSymbol {
305    #[inline]
306    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
307        write!(f, "LongSymbol({})", self.as_str())
308    }
309}
310
311impl Display for LongSymbol {
312    #[inline]
313    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
314        f.pad(self.as_str())
315    }
316}
317
318impl AsRef<str> for LongSymbol {
319    #[inline]
320    fn as_ref(&self) -> &str {
321        self.as_str()
322    }
323}
324
325impl Deref for LongSymbol {
326    type Target = str;
327
328    #[inline]
329    fn deref(&self) -> &Self::Target {
330        self.as_str()
331    }
332}
333
334impl PartialEq<Symbol> for LongSymbol {
335    #[inline]
336    fn eq(&self, other: &Symbol) -> bool {
337        match self {
338            LongSymbol::Inline(symbol) => symbol == other,
339            LongSymbol::Heap(string) => &**string == other,
340        }
341    }
342}
343
344impl PartialEq<LongSymbol> for Symbol {
345    #[inline]
346    fn eq(&self, other: &LongSymbol) -> bool {
347        other == self
348    }
349}
350
351impl PartialEq<str> for LongSymbol {
352    #[inline]
353    fn eq(&self, other: &str) -> bool {
354        match self {
355            LongSymbol::Inline(symbol) => symbol == other,
356            LongSymbol::Heap(string) => &**string == other,
357        }
358    }
359}
360
361impl PartialEq<LongSymbol> for str {
362    #[inline]
363    fn eq(&self, other: &LongSymbol) -> bool {
364        other == self
365    }
366}
367
368impl PartialEq<String> for LongSymbol {
369    #[inline]
370    fn eq(&self, other: &String) -> bool {
371        match self {
372            LongSymbol::Inline(symbol) => symbol == other,
373            LongSymbol::Heap(string) => &**string == &**other,
374        }
375    }
376}
377
378impl PartialEq<LongSymbol> for String {
379    #[inline]
380    fn eq(&self, other: &LongSymbol) -> bool {
381        other == self
382    }
383}
384
385impl PartialEq<&str> for LongSymbol {
386    #[inline]
387    fn eq(&self, other: &&str) -> bool {
388        match self {
389            LongSymbol::Inline(symbol) => symbol == *other,
390            LongSymbol::Heap(string) => &**string == *other,
391        }
392    }
393}
394
395impl PartialEq<LongSymbol> for &str {
396    #[inline]
397    fn eq(&self, other: &LongSymbol) -> bool {
398        other == self
399    }
400}
401
402impl PartialEq for LongSymbol {
403    #[inline]
404    fn eq(&self, other: &Self) -> bool {
405        match self {
406            Self::Inline(symbol) => symbol == other,
407            Self::Heap(string) => &**string == other,
408        }
409    }
410}
411
412impl Eq for LongSymbol {}
413
414#[inline]
415fn long_symbol_cmp(a: &LongSymbol, b: &LongSymbol) -> Ordering {
416    match (a, b) {
417        (LongSymbol::Inline(a), LongSymbol::Inline(b)) => a.cmp(b),
418        _ => a.as_str().cmp(b.as_str()),
419    }
420}
421
422impl PartialOrd for LongSymbol {
423    #[inline]
424    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
425        Some(long_symbol_cmp(self, other))
426    }
427}
428
429impl Ord for LongSymbol {
430    #[inline]
431    fn cmp(&self, other: &Self) -> Ordering {
432        long_symbol_cmp(self, other)
433    }
434}
435
436impl Hash for LongSymbol {
437    fn hash<H: Hasher>(&self, state: &mut H) {
438        Hash::hash(self.as_str(), state)
439    }
440}
441
442#[cfg(feature = "serde")]
443mod serde {
444    use super::*;
445    use ::serde::{
446        de::{self, Visitor},
447        Deserialize, Deserializer, Serialize, Serializer,
448    };
449
450    impl Serialize for Symbol {
451        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
452        where
453            S: Serializer,
454        {
455            serializer.serialize_str(self.as_str())
456        }
457    }
458
459    impl<'de> Deserialize<'de> for Symbol {
460        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
461        where
462            D: Deserializer<'de>,
463        {
464            deserializer.deserialize_str(SymbolVisitor)
465        }
466    }
467
468    struct SymbolVisitor;
469
470    impl<'de> Visitor<'de> for SymbolVisitor {
471        type Value = Symbol;
472
473        #[inline]
474        fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
475            f.write_str("A symbol string")
476        }
477
478        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
479        where
480            E: de::Error,
481        {
482            Symbol::from_str(v).map_err(E::custom)
483        }
484    }
485
486    impl Serialize for LongSymbol {
487        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
488        where
489            S: Serializer,
490        {
491            serializer.serialize_str(self.as_str())
492        }
493    }
494
495    impl<'de> Deserialize<'de> for LongSymbol {
496        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
497        where
498            D: Deserializer<'de>,
499        {
500            deserializer.deserialize_str(LongSymbolVisitor)
501        }
502    }
503
504    struct LongSymbolVisitor;
505
506    impl<'de> Visitor<'de> for LongSymbolVisitor {
507        type Value = LongSymbol;
508
509        #[inline]
510        fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
511            f.write_str("A string")
512        }
513
514        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
515        where
516            E: de::Error,
517        {
518            Ok(LongSymbol::from(v))
519        }
520
521        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
522        where
523            E: de::Error,
524        {
525            Ok(LongSymbol::from(v))
526        }
527    }
528}
529
530#[cfg(feature = "sqlx")]
531mod sqlx {
532    use super::*;
533    use sqlx_core::{
534        database::{Database, HasValueRef},
535        decode::Decode,
536        error::BoxDynError,
537        types::Type,
538    };
539
540    impl<'r, DB: Database> Decode<'r, DB> for Symbol
541    where
542        &'r str: Decode<'r, DB>,
543    {
544        fn decode(value: <DB as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
545            let value = <&str as Decode<DB>>::decode(value)?;
546            Self::from_str(value).map_err(Into::into)
547        }
548    }
549
550    impl<DB: Database> Type<DB> for Symbol
551    where
552        str: Type<DB>,
553    {
554        fn type_info() -> <DB as Database>::TypeInfo {
555            <str as Type<DB>>::type_info()
556        }
557    }
558
559    impl<'r, DB: Database> Decode<'r, DB> for LongSymbol
560    where
561        &'r str: Decode<'r, DB>,
562    {
563        fn decode(value: <DB as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
564            let value = <&str as Decode<DB>>::decode(value)?;
565            Ok(LongSymbol::from(value))
566        }
567    }
568
569    impl<DB: Database> Type<DB> for LongSymbol
570    where
571        str: Type<DB>,
572    {
573        fn type_info() -> <DB as Database>::TypeInfo {
574            <str as Type<DB>>::type_info()
575        }
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn sizes() {
585        use std::mem::size_of;
586
587        assert_eq!(size_of::<Symbol>(), 8);
588        assert_eq!(size_of::<Option<Symbol>>(), 8);
589        assert_eq!(size_of::<Result<Symbol, InvalidSymbol>>(), 8);
590
591        assert_eq!(size_of::<LongSymbol>(), 16);
592    }
593
594    #[test]
595    fn creation() {
596        assert!(Symbol::from_str("AAPL").is_ok());
597        assert!(Symbol::from_str("HSBC.A").is_ok());
598        assert!(Symbol::from_str("this is too long").is_err());
599        assert!(Symbol::from_str("").is_err());
600
601        assert!(matches!(LongSymbol::from("AAPL"), LongSymbol::Inline(_)));
602        assert!(matches!(
603            LongSymbol::from("this is too long"),
604            LongSymbol::Heap(_)
605        ));
606        assert!(matches!(LongSymbol::from(""), LongSymbol::Heap(_)));
607    }
608
609    #[test]
610    fn equality() {
611        let sym1 = Symbol::from_str("ABCDEFG").unwrap();
612        let sym2 = Symbol::from_str("ABCDEFG").unwrap();
613        let sym3 = Symbol::from_str("ABCDEF").unwrap();
614
615        assert_eq!(sym1, sym2);
616        assert_ne!(sym1, sym3);
617        assert_ne!(sym2, sym3);
618
619        let lsym1 = LongSymbol::from(sym1);
620        let lsym2 = LongSymbol::Heap(Box::from("ABCDEFG"));
621        let lsym3 = LongSymbol::from(sym3);
622
623        assert_eq!(lsym1, lsym2);
624        assert_eq!(sym1, lsym2);
625        assert_eq!(lsym1, sym2);
626        assert_ne!(sym1, lsym3);
627        assert_ne!(sym2, lsym3);
628        assert_eq!(sym3, lsym3);
629    }
630
631    #[test]
632    fn str_equality() {
633        let sym1 = Symbol::from_str("ABCDEFG").unwrap();
634        let sym2 = Symbol::from_str("ABCDEF").unwrap();
635        let str1 = "ABCDEFG";
636        let str2 = "ABCDEF";
637
638        assert_eq!(sym1, str1);
639        assert_eq!(sym2, str2);
640        assert_ne!(sym1, str2);
641        assert_ne!(sym2, str1);
642
643        let lsym1 = LongSymbol::from("this is too long");
644        assert_eq!(lsym1, "this is too long");
645        assert_ne!(lsym1, "short");
646    }
647
648    #[test]
649    fn ord() {
650        let symbols = ["A", "AA", "AB", "B", "BBB", "C", "CA", "CBS"]
651            .map(|symbol| Symbol::from_str(symbol).unwrap());
652
653        for (i, sym1) in symbols.into_iter().enumerate() {
654            for (j, sym2) in symbols.into_iter().enumerate() {
655                assert_eq!(i.cmp(&j), sym1.cmp(&sym2));
656            }
657        }
658
659        let long_symbols = [
660            "",
661            "A",
662            "AA",
663            "AB",
664            "ABRACADABRA",
665            "B",
666            "BBB",
667            "BBBBBBBB",
668            "C",
669            "CA",
670            "CALIFORNIA",
671            "CBS",
672            "VERYLONGSYMBOL",
673        ]
674        .map(LongSymbol::from);
675
676        for (i, sym1) in long_symbols.iter().enumerate() {
677            for (j, sym2) in long_symbols.iter().enumerate() {
678                assert_eq!(i.cmp(&j), sym1.cmp(sym2));
679            }
680        }
681    }
682
683    #[test]
684    fn formatting() {
685        let symbol = Symbol::from_str("FOO").unwrap();
686
687        assert_eq!(format!("{symbol}"), "FOO");
688        assert_eq!(format!("{symbol:<5}"), "FOO  ");
689        assert_eq!(format!("{symbol:>5}"), "  FOO");
690        assert_eq!(format!("{symbol:^5}"), " FOO ");
691
692        let long_symbol = LongSymbol::from("LONGSYMBOL");
693        assert_eq!(format!("{long_symbol}"), "LONGSYMBOL");
694        assert_eq!(format!("{long_symbol:<16}"), "LONGSYMBOL      ");
695        assert_eq!(format!("{long_symbol:>16}"), "      LONGSYMBOL");
696        assert_eq!(format!("{long_symbol:^16}"), "   LONGSYMBOL   ");
697    }
698}