Skip to content

Commit d16538e

Browse files
authored
Use newer const fn features
Existing methods made `const fn`: * `AsciiStr::as_str()` * `AsciiStr::as_bytes()` * `AsciiStr::trim()` * `AsciiStr::trim_left()` * `AsciiStr::trim_right()` * `AsciiChar::from_ascii_unchecked()` * `AsciiChar::as_printable_char()` New methods: * `AsciiStr::new([AsciiChar])` * `AsciiStr::from_ascii_bytes()` * `AsciiStr::from_ascii_str()` * `AsciiChar::try_new(char)` Also switch back to `&&` and `||` in many methods. This increases MSRV to 1.56.
2 parents 8605175 + 94b8470 commit d16538e

File tree

5 files changed

+137
-48
lines changed

5 files changed

+137
-48
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
rust: [1.41.1, stable, beta, nightly]
16+
rust: [1.56.1, stable, beta, nightly]
1717
steps:
1818
- uses: actions/checkout@v2
1919
- uses: hecrj/setup-rust-action@v1

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ ascii = { version = "1.1", default-features = false, features = ["alloc"] }
3535

3636
## Minimum supported Rust version
3737

38-
The minimum Rust version for 1.1.\* releases is 1.41.1.
38+
The minimum Rust version for 1.2.\* releases is 1.56.1.
3939
Later 1.y.0 releases might require newer Rust versions, but the three most
4040
recent stable releases at the time of publishing will always be supported.
4141
For example this means that if the current stable Rust version is 1.70 when
42-
ascii 1.2.0 is released, then ascii 1.2.\* will not require a newer
42+
ascii 1.3.0 is released, then ascii 1.3.\* will not require a newer
4343
Rust version than 1.68.
4444

4545
## History

src/ascii_char.rs

+43-16
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,32 @@ impl AsciiChar {
360360
ALL[ch as usize]
361361
}
362362

363+
/// Create an `AsciiChar` from a `char`, in a `const fn` way.
364+
///
365+
/// Within non-`const fn` functions the more general
366+
/// [`from_ascii()`](#method.from_ascii) should be used instead.
367+
///
368+
/// # Examples
369+
/// ```
370+
/// # use ascii::AsciiChar;
371+
/// assert!(AsciiChar::try_new('-').is_ok());
372+
/// assert!(AsciiChar::try_new('—').is_err());
373+
/// assert_eq!(AsciiChar::try_new('\x7f'), Ok(AsciiChar::DEL));
374+
/// ```
375+
///
376+
/// # Errors
377+
///
378+
/// Fails for non-ASCII characters.
379+
#[inline]
380+
pub const fn try_new(ch: char) -> Result<Self, ToAsciiCharError> {
381+
unsafe {
382+
match ch as u32 {
383+
0..=127 => Ok(mem::transmute(ch as u8)),
384+
_ => Err(ToAsciiCharError(())),
385+
}
386+
}
387+
}
388+
363389
/// Constructs an ASCII character from a `u8`, `char` or other character
364390
/// type without any checks.
365391
///
@@ -375,9 +401,9 @@ impl AsciiChar {
375401
/// and `Some(AsciiChar::from_ascii_unchecked(128))` might be `None`.
376402
#[inline]
377403
#[must_use]
378-
pub unsafe fn from_ascii_unchecked(ch: u8) -> Self {
404+
pub const unsafe fn from_ascii_unchecked(ch: u8) -> Self {
379405
// SAFETY: Caller guarantees `ch` is within bounds of ascii.
380-
unsafe { ch.to_ascii_char_unchecked() }
406+
unsafe { mem::transmute(ch) }
381407
}
382408

383409
/// Converts an ASCII character into a `u8`.
@@ -411,7 +437,7 @@ impl AsciiChar {
411437
#[inline]
412438
#[must_use]
413439
pub const fn is_alphabetic(self) -> bool {
414-
(self.to_not_upper() >= b'a') & (self.to_not_upper() <= b'z')
440+
(self.to_not_upper() >= b'a') && (self.to_not_upper() <= b'z')
415441
}
416442

417443
/// Check if the character is a letter (a-z, A-Z).
@@ -457,14 +483,14 @@ impl AsciiChar {
457483
#[inline]
458484
#[must_use]
459485
pub const fn is_ascii_digit(&self) -> bool {
460-
(*self as u8 >= b'0') & (*self as u8 <= b'9')
486+
(*self as u8 >= b'0') && (*self as u8 <= b'9')
461487
}
462488

463489
/// Check if the character is a letter or number
464490
#[inline]
465491
#[must_use]
466492
pub const fn is_alphanumeric(self) -> bool {
467-
self.is_alphabetic() | self.is_ascii_digit()
493+
self.is_alphabetic() || self.is_ascii_digit()
468494
}
469495

470496
/// Check if the character is a letter or number
@@ -491,7 +517,7 @@ impl AsciiChar {
491517
#[inline]
492518
#[must_use]
493519
pub const fn is_ascii_blank(&self) -> bool {
494-
(*self as u8 == b' ') | (*self as u8 == b'\t')
520+
(*self as u8 == b' ') || (*self as u8 == b'\t')
495521
}
496522

497523
/// Check if the character one of ' ', '\t', '\n', '\r',
@@ -500,7 +526,7 @@ impl AsciiChar {
500526
#[must_use]
501527
pub const fn is_whitespace(self) -> bool {
502528
let b = self as u8;
503-
self.is_ascii_blank() | (b == b'\n') | (b == b'\r') | (b == 0x0b) | (b == 0x0c)
529+
self.is_ascii_blank() || (b == b'\n') || (b == b'\r') || (b == 0x0b) || (b == 0x0c)
504530
}
505531

506532
/// Check if the character is a ' ', '\t', '\n', '\r' or '\0xc' (form feed).
@@ -510,9 +536,9 @@ impl AsciiChar {
510536
#[must_use]
511537
pub const fn is_ascii_whitespace(&self) -> bool {
512538
self.is_ascii_blank()
513-
| (*self as u8 == b'\n')
514-
| (*self as u8 == b'\r')
515-
| (*self as u8 == 0x0c/*form feed*/)
539+
|| (*self as u8 == b'\n')
540+
|| (*self as u8 == b'\r')
541+
|| (*self as u8 == 0x0c/*form feed*/)
516542
}
517543

518544
/// Check if the character is a control character
@@ -530,7 +556,7 @@ impl AsciiChar {
530556
#[inline]
531557
#[must_use]
532558
pub const fn is_ascii_control(&self) -> bool {
533-
((*self as u8) < b' ') | (*self as u8 == 127)
559+
((*self as u8) < b' ') || (*self as u8 == 127)
534560
}
535561

536562
/// Checks if the character is printable (except space)
@@ -624,7 +650,7 @@ impl AsciiChar {
624650
#[inline]
625651
#[must_use]
626652
pub const fn is_ascii_punctuation(&self) -> bool {
627-
self.is_ascii_graphic() & !self.is_alphanumeric()
653+
self.is_ascii_graphic() && !self.is_alphanumeric()
628654
}
629655

630656
/// Checks if the character is a valid hex digit
@@ -641,7 +667,7 @@ impl AsciiChar {
641667
#[inline]
642668
#[must_use]
643669
pub const fn is_ascii_hexdigit(&self) -> bool {
644-
self.is_ascii_digit() | ((*self as u8 | 0x20_u8).wrapping_sub(b'a') < 6)
670+
self.is_ascii_digit() || ((*self as u8 | 0x20u8).wrapping_sub(b'a') < 6)
645671
}
646672

647673
/// Unicode has printable versions of the ASCII control codes, like '␛'.
@@ -659,14 +685,15 @@ impl AsciiChar {
659685
/// assert_eq!(AsciiChar::new('p').as_printable_char(), 'p');
660686
/// ```
661687
#[must_use]
662-
pub fn as_printable_char(self) -> char {
688+
pub const fn as_printable_char(self) -> char {
689+
#![allow(clippy::transmute_int_to_char)] // from_utf32_unchecked() is not const fn yet.
663690
match self as u8 {
664691
// Non printable characters
665692
// SAFETY: From codepoint 0x2400 ('␀') to 0x241f (`␟`), there are characters representing
666693
// the unprintable characters from 0x0 to 0x1f, ordered correctly.
667694
// As `b` is guaranteed to be within 0x0 to 0x1f, the conversion represents a
668695
// valid character.
669-
b @ 0x0..=0x1f => unsafe { char::from_u32_unchecked(u32::from('␀') + u32::from(b)) },
696+
b @ 0x0..=0x1f => unsafe { mem::transmute('␀' as u32 + b as u32) },
670697

671698
// 0x7f (delete) has it's own character at codepoint 0x2420, not 0x247f, so it is special
672699
// cased to return it's character
@@ -728,7 +755,7 @@ impl AsciiChar {
728755
#[must_use]
729756
pub const fn eq_ignore_ascii_case(&self, other: &Self) -> bool {
730757
(self.as_byte() == other.as_byte())
731-
| (self.is_alphabetic() & (self.to_not_upper() == other.to_not_upper()))
758+
|| (self.is_alphabetic() && (self.to_not_upper() == other.to_not_upper()))
732759
}
733760
}
734761

src/ascii_str.rs

+89-27
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use alloc::borrow::ToOwned;
33
#[cfg(feature = "alloc")]
44
use alloc::boxed::Box;
5-
use core::fmt;
5+
use core::{fmt, mem};
66
use core::ops::{Index, IndexMut};
77
use core::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
88
use core::slice::{self, Iter, IterMut, SliceIndex};
@@ -28,20 +28,37 @@ pub struct AsciiStr {
2828
}
2929

3030
impl AsciiStr {
31+
/// Coerces into an `AsciiStr` slice.
32+
///
33+
/// # Examples
34+
/// ```
35+
/// # use ascii::{AsciiChar, AsciiStr};
36+
/// const HELLO: &AsciiStr = AsciiStr::new(
37+
/// &[AsciiChar::H, AsciiChar::e, AsciiChar::l, AsciiChar::l, AsciiChar::o]
38+
/// );
39+
///
40+
/// assert_eq!(HELLO.as_str(), "Hello");
41+
/// ```
42+
#[inline]
43+
#[must_use]
44+
pub const fn new(s: &[AsciiChar]) -> &Self {
45+
unsafe { mem::transmute(s) }
46+
}
47+
3148
/// Converts `&self` to a `&str` slice.
3249
#[inline]
3350
#[must_use]
34-
pub fn as_str(&self) -> &str {
51+
pub const fn as_str(&self) -> &str {
3552
// SAFETY: All variants of `AsciiChar` are valid bytes for a `str`.
36-
unsafe { &*(self as *const AsciiStr as *const str) }
53+
unsafe { mem::transmute(self) }
3754
}
3855

3956
/// Converts `&self` into a byte slice.
4057
#[inline]
4158
#[must_use]
42-
pub fn as_bytes(&self) -> &[u8] {
59+
pub const fn as_bytes(&self) -> &[u8] {
4360
// SAFETY: All variants of `AsciiChar` are valid `u8`, given they're `repr(u8)`.
44-
unsafe { &*(self as *const AsciiStr as *const [u8]) }
61+
unsafe { mem::transmute(self) }
4562
}
4663

4764
/// Returns the entire string as slice of `AsciiChar`s.
@@ -108,6 +125,53 @@ impl AsciiStr {
108125
bytes.as_ref().as_ascii_str()
109126
}
110127

128+
/// Convert a byte slice innto an `AsciiStr`.
129+
///
130+
/// [`from_ascii()`](#method.from_ascii) should be preferred outside of `const` contexts
131+
/// as it might be faster due to using functions that are not `const fn`.
132+
///
133+
/// # Errors
134+
/// Returns `Err` if not all bytes are valid ASCII values.
135+
///
136+
/// # Examples
137+
/// ```
138+
/// # use ascii::AsciiStr;
139+
/// assert!(AsciiStr::from_ascii_bytes(b"\x00\x22\x44").is_ok());
140+
/// assert!(AsciiStr::from_ascii_bytes(b"\x66\x77\x88").is_err());
141+
/// ```
142+
pub const fn from_ascii_bytes(b: &[u8]) -> Result<&Self, AsAsciiStrError> {
143+
#![allow(clippy::indexing_slicing)] // .get() is not const yes (as of Rust 1.61)
144+
let mut valid = 0;
145+
loop {
146+
if valid == b.len() {
147+
// SAFETY: `is_ascii` having returned true for all bytes guarantees all bytes are within ascii range.
148+
return unsafe { Ok(mem::transmute(b)) };
149+
} else if b[valid].is_ascii() {
150+
valid += 1;
151+
} else {
152+
return Err(AsAsciiStrError(valid));
153+
}
154+
}
155+
}
156+
157+
/// Convert a `str` innto an `AsciiStr`.
158+
///
159+
/// [`from_ascii()`](#method.from_ascii) should be preferred outside of `const` contexts
160+
/// as it might be faster due to using functions that are not `const fn`.
161+
///
162+
/// # Errors
163+
/// Returns `Err` if it contains non-ASCII codepoints.
164+
///
165+
/// # Examples
166+
/// ```
167+
/// # use ascii::AsciiStr;
168+
/// assert!(AsciiStr::from_ascii_str("25 C").is_ok());
169+
/// assert!(AsciiStr::from_ascii_str("35°C").is_err());
170+
/// ```
171+
pub const fn from_ascii_str(s: &str) -> Result<&Self, AsAsciiStrError> {
172+
Self::from_ascii_bytes(s.as_bytes())
173+
}
174+
111175
/// Converts anything that can be represented as a byte slice to an `AsciiStr` without checking
112176
/// for non-ASCII characters..
113177
///
@@ -214,7 +278,7 @@ impl AsciiStr {
214278
/// assert_eq!("white \tspace", example.trim());
215279
/// ```
216280
#[must_use]
217-
pub fn trim(&self) -> &Self {
281+
pub const fn trim(&self) -> &Self {
218282
self.trim_start().trim_end()
219283
}
220284

@@ -227,14 +291,16 @@ impl AsciiStr {
227291
/// assert_eq!("white \tspace \t", example.trim_start());
228292
/// ```
229293
#[must_use]
230-
pub fn trim_start(&self) -> &Self {
231-
let whitespace_len = self
232-
.chars()
233-
.position(|ch| !ch.is_whitespace())
234-
.unwrap_or_else(|| self.len());
235-
236-
// SAFETY: `whitespace_len` is `0..=len`, which is at most `len`, which is a valid empty slice.
237-
unsafe { self.as_slice().get_unchecked(whitespace_len..).into() }
294+
pub const fn trim_start(&self) -> &Self {
295+
let mut trimmed = &self.slice;
296+
while let Some((first, rest)) = trimmed.split_first() {
297+
if first.is_whitespace() {
298+
trimmed = rest;
299+
} else {
300+
break;
301+
}
302+
}
303+
AsciiStr::new(trimmed)
238304
}
239305

240306
/// Returns an ASCII string slice with trailing whitespace removed.
@@ -246,20 +312,16 @@ impl AsciiStr {
246312
/// assert_eq!(" \twhite \tspace", example.trim_end());
247313
/// ```
248314
#[must_use]
249-
pub fn trim_end(&self) -> &Self {
250-
// Number of whitespace characters counting from the end
251-
let whitespace_len = self
252-
.chars()
253-
.rev()
254-
.position(|ch| !ch.is_whitespace())
255-
.unwrap_or_else(|| self.len());
256-
257-
// SAFETY: `whitespace_len` is `0..=len`, which is at most `len`, which is a valid empty slice, and at least `0`, which is the whole slice.
258-
unsafe {
259-
self.as_slice()
260-
.get_unchecked(..self.len() - whitespace_len)
261-
.into()
315+
pub const fn trim_end(&self) -> &Self {
316+
let mut trimmed = &self.slice;
317+
while let Some((last, rest)) = trimmed.split_last() {
318+
if last.is_whitespace() {
319+
trimmed = rest;
320+
} else {
321+
break;
322+
}
262323
}
324+
AsciiStr::new(trimmed)
263325
}
264326

265327
/// Compares two strings case-insensitively.

src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
//!
1616
//! # Minimum supported Rust version
1717
//!
18-
//! The minimum Rust version for 1.1.\* releases is 1.41.1.
18+
//! The minimum Rust version for 1.2.\* releases is 1.56.1.
1919
//! Later 1.y.0 releases might require newer Rust versions, but the three most
2020
//! recent stable releases at the time of publishing will always be supported.
2121
//! For example this means that if the current stable Rust version is 1.70 when
22-
//! ascii 1.2.0 is released, then ascii 1.2.\* will not require a newer
22+
//! ascii 1.3.0 is released, then ascii 1.3.\* will not require a newer
2323
//! Rust version than 1.68.
2424
//!
2525
//! # History

0 commit comments

Comments
 (0)