From d2d7da19739a0409b78609c442787d007729b391 Mon Sep 17 00:00:00 2001 From: Alex Povel Date: Sun, 11 Jun 2023 17:01:39 +0200 Subject: [PATCH] feat: Introduce `SearchError` to have a proper `Error` Originally removed in 10e39a538284be8d0e2763f61122d8bc8eba5414 --- src/lib.rs | 59 +++++++++++++++++++++++++++++++++++++-------------- tests/main.rs | 44 +++++++++++++++++++------------------- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5ec98d7..7cb6d27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,6 +92,33 @@ impl Display for SortedString<'_> { } } +/// Error returned in case of a failed search. +/// +/// [Thin wrapper](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) +/// for [`Range`], required to implement [`Error`], achieving a [useful +/// API](https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SearchError( + /// Location of the last *unsuccessful* comparison. + /// + /// This is *not* the location where the needle could be inserted. That location is + /// either to the left *or* right of this location, depending on how + /// [comparison](https://doc.rust-lang.org/std/primitive.str.html#impl-PartialOrd%3Cstr%3E-for-str) + /// goes. As this error is not particularly actionable at runtime, the exact + /// location (left, right) is not reported, saving computation at runtime and + /// affording a simpler implementation. + pub Range, +); + +impl Error for SearchError {} + +impl Display for SearchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let SearchError(range) = self; + write!(f, "needle not found, last looked at {range:?}") + } +} + /// The result of a [`SortedString::binary_search()`]. /// /// @@ -101,8 +128,8 @@ impl Display for SortedString<'_> { /// /// ## Error case /// -/// The location of the last *unsuccessful* comparison. -pub type SearchResult = Result, Range>; +/// Refer to [`SearchError`] for details. +pub type SearchResult = Result, SearchError>; impl<'a> SortedString<'a> { /// Creates a new instance of [`SortedString`], performing sanity checks. @@ -223,7 +250,7 @@ impl<'a> SortedString<'a> { /// /// let needle = "Angel"; /// let result = ss.binary_search(needle); - /// assert_eq!(result, Err(Range { start: 0, end: 0 })); + /// assert_eq!(result, Err(b4s::SearchError(Range { start: 0, end: 0 }))); /// /// let needle = "ngel\n"; /// let result = ss.binary_search(needle); @@ -232,15 +259,7 @@ impl<'a> SortedString<'a> { /// /// # Errors /// - /// There is only a single error case: the needle is not found. In that case, the - /// location of the last *unsuccessful* comparison is returned. - /// - /// This is *not* the location where the needle could be inserted. That location is - /// either to the left *or* right of this location, depending on how - /// [comparison](https://doc.rust-lang.org/std/primitive.str.html#impl-PartialOrd%3Cstr%3E-for-str) - /// goes. As this error is not particularly actionable at runtime, the exact - /// location (left, right) is not reported, saving computation at runtime and - /// affording a simpler implementation. + /// Refer to [`SearchError`] for more info. /// /// # Panics /// @@ -394,13 +413,13 @@ impl<'a> SortedString<'a> { } } - Err(Range { start, end }) + Err(SearchError(Range { start, end })) } /// Creates an instance of [`SortedString`] without performing sanity checks. /// /// This is essentially what conventionally would be a simple - /// [`new()`](https://rust-lang.github.io/api-guidelines/naming.html#casing-conforms-to-rfc-430-c-case), + /// [`new()`](https://rust-lang.github.io/api-guidelines/interoperability.html#types-eagerly-implement-common-traits-c-common-traits), /// but specifically named to alert users to the dangers. /// /// # Example: Simple Use @@ -414,12 +433,17 @@ impl<'a> SortedString<'a> { /// # Example: Incorrect Use /// /// ``` + /// use std::ops::Range; + /// /// let sep = b4s::AsciiChar::Comma; /// let unsorted_haystack = "a,c,b"; /// let sorted_string = b4s::SortedString::new_unchecked(unsorted_haystack, sep); /// /// // Unable to find element in unsorted haystack - /// assert_eq!(sorted_string.binary_search("b"), Err(std::ops::Range { start: 0, end: 1 })); + /// assert_eq!( + /// sorted_string.binary_search("b"), + /// Err(b4s::SearchError(Range { start: 0, end: 1 })) + /// ); /// ``` #[must_use] pub const fn new_unchecked(string: &'a str, sep: AsciiChar) -> Self { @@ -444,7 +468,10 @@ impl<'a> SortedString<'a> { /// // Passes fine /// let sorted_string = b4s::SortedString::new_checked(&sorted_haystack, sep).unwrap(); /// - /// assert_eq!(sorted_string.binary_search("c"), Ok(std::ops::Range { start: 4, end: 5 })); + /// assert_eq!( + /// sorted_string.binary_search("c"), + /// Ok(std::ops::Range { start: 4, end: 5 }) + /// ); /// ``` #[must_use] pub fn sort(string: &str, sep: AsciiChar) -> String { diff --git a/tests/main.rs b/tests/main.rs index 8fc6af7..9fddf33 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,5 +1,5 @@ use ascii::AsciiChar; -use b4s::{SearchResult, SortedString}; +use b4s::{SearchError, SearchResult, SortedString}; use rstest::rstest; use std::ops::Range; @@ -73,9 +73,9 @@ fn test_base_cases( } #[rstest] -#[case("mn", "abc,mno,yz", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] -#[case("a", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] -#[case("z", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 8, end: 10 }))] +#[case("mn", "abc,mno,yz", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] +#[case("a", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] +#[case("z", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 8, end: 10 })))] fn test_needle_shorter_than_any_haystack_item( #[case] needle: &str, #[case] haystack: &str, @@ -86,9 +86,9 @@ fn test_needle_shorter_than_any_haystack_item( } #[rstest] -#[case("abcd", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] -#[case("xyz", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 4, end: 7 }))] -#[case("zyz", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 8, end: 10 }))] +#[case("abcd", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] +#[case("xyz", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 4, end: 7 })))] +#[case("zyz", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 8, end: 10 })))] fn test_needle_longer_than_any_haystack_item( #[case] needle: &str, #[case] haystack: &str, @@ -99,7 +99,7 @@ fn test_needle_longer_than_any_haystack_item( } #[rstest] -#[case("abc", "a,b,c", AsciiChar::Comma, Err(Range { start: 0, end: 1 }))] +#[case("abc", "a,b,c", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 1 })))] fn test_single_character_haystack( #[case] needle: &str, #[case] haystack: &str, @@ -111,9 +111,9 @@ fn test_single_character_haystack( #[rstest] #[case("a", "a,def,yz", AsciiChar::Comma, Ok(Range { start: 0, end: 1 }))] -#[case("a", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] +#[case("a", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] #[case("z", "abc,def,z", AsciiChar::Comma, Ok(Range { start: 8, end: 9 }))] -#[case("z", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 8, end: 10 }))] +#[case("z", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 8, end: 10 })))] fn test_single_character_needle( #[case] needle: &str, #[case] haystack: &str, @@ -126,7 +126,7 @@ fn test_single_character_needle( #[rstest] #[case("a", "a,b,c", AsciiChar::Comma, Ok(Range { start: 0, end: 1 }))] #[case("c", "a,b,c", AsciiChar::Comma, Ok(Range { start: 4, end: 5 }))] -#[case("d", "a,b,c", AsciiChar::Comma, Err(Range { start: 4, end: 5 }))] +#[case("d", "a,b,c", AsciiChar::Comma, Err(SearchError(Range { start: 4, end: 5 })))] fn test_single_character_needle_and_haystack( #[case] needle: &str, #[case] haystack: &str, @@ -138,9 +138,9 @@ fn test_single_character_needle_and_haystack( #[rstest] #[case("aaa", "aaa,def,yz", AsciiChar::Comma, Ok(Range { start: 0, end: 3 }))] -#[case("aaa", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] +#[case("aaa", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] #[case("zzz", "abc,def,zzz", AsciiChar::Comma, Ok(Range { start: 8, end: 11 }))] -#[case("zzz", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 8, end: 10 }))] +#[case("zzz", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 8, end: 10 })))] fn test_repeated_character_needle( #[case] needle: &str, #[case] haystack: &str, @@ -153,7 +153,7 @@ fn test_repeated_character_needle( #[rstest] #[case("", ",", AsciiChar::Comma, Ok(Range { start: 0, end: 0 }))] #[case("", ",,", AsciiChar::Comma, Ok(Range { start: 1, end: 1 }))] -#[case("", "abc", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] +#[case("", "abc", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] fn test_empty_needle( #[case] needle: &str, #[case] haystack: &str, @@ -166,7 +166,7 @@ fn test_empty_needle( #[rstest] #[case("abc", "abc", AsciiChar::Comma, Ok(Range { start: 0, end: 3 }))] #[case("abc", "abc,def", AsciiChar::Comma, Ok(Range { start: 0, end: 3 }))] -#[case("abc", ",", AsciiChar::Comma, Err(Range { start: 0, end: 0 }))] +#[case("abc", ",", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 0 })))] #[case("", ",,,,", AsciiChar::Comma, Ok(Range { start: 2, end: 2 }))] #[case("abc", ",,,abc", AsciiChar::Comma, Ok(Range { start: 3, end: 6 }))] fn test_oddly_shaped_haystack( @@ -179,8 +179,8 @@ fn test_oddly_shaped_haystack( } #[rstest] -#[case("nmo", "abc,mno,yz", AsciiChar::Comma, Err(Range { start: 4, end: 7 }))] -#[case("cba", "abc,def,yz", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] +#[case("nmo", "abc,mno,yz", AsciiChar::Comma, Err(SearchError(Range { start: 4, end: 7 })))] +#[case("cba", "abc,def,yz", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] fn test_switched_characters_needle( #[case] needle: &str, #[case] haystack: &str, @@ -193,7 +193,7 @@ fn test_switched_characters_needle( #[rstest] #[case("abc", "abc-def-yz", AsciiChar::Minus, Ok(Range { start: 0, end: 3 }))] #[case("abc", "abc\0def\0yz", AsciiChar::Null, Ok(Range { start: 0, end: 3 }))] -#[case("defg", "abc\0def\0yz", AsciiChar::Null, Err(Range { start: 4, end: 7 }))] +#[case("defg", "abc\0def\0yz", AsciiChar::Null, Err(SearchError(Range { start: 4, end: 7 })))] fn test_different_separators( #[case] needle: &str, #[case] haystack: &str, @@ -204,8 +204,8 @@ fn test_different_separators( } #[rstest] -#[case("abc", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Err(Range { start: 11, end: 15 }))] -#[case("ABC", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Err(Range { start: 0, end: 4 }))] +#[case("abc", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Err(SearchError(Range { start: 11, end: 15 })))] +#[case("ABC", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Err(SearchError(Range { start: 0, end: 4 })))] #[case("Hund", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Ok(Range { start: 0, end: 4 }))] #[case("Katze", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Ok(Range { start: 5, end: 10 }))] #[case("Maus", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Ok(Range { start: 11, end: 15 }))] @@ -219,7 +219,7 @@ fn test_real_world_examples( } #[rstest] -#[case("Hündin", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Err(Range { start: 5, end: 10 }))] +#[case("Hündin", "Hund\nKatze\nMaus", AsciiChar::LineFeed, Err(SearchError(Range { start: 5, end: 10 })))] #[case("Hündin", "Hündin\nKatze\nMaus", AsciiChar::LineFeed, Ok(Range { start: 0, end: 7 }))] #[case( "Mäuschen", @@ -259,7 +259,7 @@ fn test_real_world_examples_with_common_prefixes( } #[rstest] -#[case("abc,def", "abc,def,ghi", AsciiChar::Comma, Err(Range { start: 0, end: 3 }))] +#[case("abc,def", "abc,def,ghi", AsciiChar::Comma, Err(SearchError(Range { start: 0, end: 3 })))] fn test_needle_contains_separator( #[case] needle: &str, #[case] haystack: &str,