Skip to content

Commit

Permalink
feat: Introduce SearchError to have a proper Error
Browse files Browse the repository at this point in the history
Originally removed in 10e39a5
  • Loading branch information
alexpovel committed Jun 11, 2023
1 parent 1b4f8f3 commit d2d7da1
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 38 deletions.
59 changes: 43 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>`], 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<usize>,
);

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()`].
///
///
Expand All @@ -101,8 +128,8 @@ impl Display for SortedString<'_> {
///
/// ## Error case
///
/// The location of the last *unsuccessful* comparison.
pub type SearchResult = Result<Range<usize>, Range<usize>>;
/// Refer to [`SearchError`] for details.
pub type SearchResult = Result<Range<usize>, SearchError>;

impl<'a> SortedString<'a> {
/// Creates a new instance of [`SortedString`], performing sanity checks.
Expand Down Expand Up @@ -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);
Expand All @@ -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
///
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
44 changes: 22 additions & 22 deletions tests/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ascii::AsciiChar;
use b4s::{SearchResult, SortedString};
use b4s::{SearchError, SearchResult, SortedString};
use rstest::rstest;
use std::ops::Range;

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 }))]
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit d2d7da1

Please sign in to comment.