2

I'm getting into Rust and Arduino at the same time.

I was programming my LCD display to show a long string by rotating it through the top column of characters. Means: Every second I shift all characters by one position and show the new String.

This was fairly complex in the Arduino language, especially because I had to know the size of the String at compile time (given my limited knowledge).

Since I'd like to use Rust in the long term, I was curious to see if that could be done more easily in a modern language. Not so much.

This is the code I came up with, after hours of experimentation:

#![no_std]

extern crate alloc;

use alloc::{vec::Vec};

fn main() {
}

fn rotate_by<T: Copy>(rotate: Vec<T>, by: isize) -> Vec<T> {
    let real_by = modulo(by, rotate.len() as isize) as usize;
    Vec::from_iter(rotate[real_by..].iter().chain(rotate[..real_by].iter()).cloned())
}

fn modulo(a: isize, b: isize) -> isize {
    a - b * (a as f64 /b as f64).floor() as isize
}

mod tests {
    use super::*;

    #[test]
    fn test_rotate_five() {
        let chars: Vec<_> = "I am the string and you should rotate me!   ".chars().collect();

        let res_chars: Vec<_> = "the string and you should rotate me!   I am ".chars().collect();

        assert_eq!(rotate_by(chars, 5), res_chars);
    }
}

My questions are:

  • Could you provide an optimized version of this function? I'm aware that there already is Vec::rotate but it uses unsafe code and can panic, which I would like to avoid (by returning a Result).
  • Explain whether or not it is possible to achieve this in-place without unsafe code (I failed).
  • Is Vec<_> the most efficient data structure to work with? I tried hard to use [char], which I thought would be more efficient, but then I have to know the size at compile time, which hardly works. I thought Rust arrays would be similar to Java arrays, which can be sized at runtime yet are also fixed size once created, but they seem to have a lot more constraints.
  • Oh and also what happens if I index into a vector at an invalid index? Will it panic? Can I do this better? Without "manually" checking the validity of the slice indices?

I realize that's a lot of questions, but I'm struggling and this is bugging me a lot, so if somebody could set me straight it would be much appreciated!

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
NoBullsh1t
  • 483
  • 4
  • 14
  • 2
    Perhaps instead of rotating the string data itself, display it in two parts at a regularly advancing and wrapping offset. Presumably you can arrange for the display routine to accept a slice of your string, and you'd simply shift the two slices you take. Probably far less compute time and less memory churn doing it this way. At index 0, you display the whole string the slice [0..], and index 1, display the slice [1..] then [0..1], and so on. – Paul Dempsey Feb 18 '23 at 01:49
  • 1
    You've asked a lot of questions there. 1+2 - see my answer. 3 - Yes, if you need it to be arbitrarily growable, but if it's small you can use [arrayvec](https://docs.rs/arrayvec) for stack-allocated vectors with a maximum capacity. 4 - with `vector[index]` it will panic, but you can use `vector.get(index)` instead, which returns `Option` and `None` if the index is out of bounds. – Peter Hall Feb 18 '23 at 02:04
  • Just as a clarification, there is no Arduino language, it is pure C++. Rust is modern compared to C++ due to it being released a few years ago compared to C++, but that says nothing. C/C++ is used regularly in a lot of places/systems. – matiaslauriti Feb 18 '23 at 04:58
  • @PaulDempsey yes, I was planning to try that, but I got hung up on the rotation issue. I want this done to understand it. – NoBullsh1t Feb 20 '23 at 10:03
  • @PeterHall regarding 4: does `vector.get(index)` or something similar also work for slices, or just individual elements? – NoBullsh1t Feb 20 '23 at 10:04
  • @matiaslauriti thanks for the clarifications. The tutorial I was following did not care too much about this detail :D – NoBullsh1t Feb 20 '23 at 10:05

1 Answers1

4

You can use slice::rotate_left and slice::rotate_right:

#![no_std]

extern crate alloc;
use alloc::vec::Vec;

fn rotate_by<T>(data: &mut [T], by: isize) {
    if by > 0 {
        data.rotate_left(by.unsigned_abs());
    } else {
        data.rotate_right(by.unsigned_abs());
    }
}

I made it rotate in-place because that is more efficient. If you don't want to do it in-place you still have the option of cloning the vector first, so this is more flexible than if the function creates a new vector, as you have done, because you aren't be able to opt out of that when you call it.

Notice that rotate_by takes a mutable slice, but you can still pass a mutable reference to a vector, because of deref coercion.

#[test]
fn test_rotate_five() {
    let mut chars: Vec<_> = "I am the string and you should rotate me!   ".chars().collect();
    let res_chars: Vec<_> = "the string and you should rotate me!   I am ".chars().collect();
    rotate_by(&mut chars, 5);
    assert_eq!(chars, res_chars);
}

There are some edge cases with moving chars around like this because some valid UTF-8 will contain grapheme clusters that are made up of multiple codepoints (chars in Rust). This will result in strange effects when a grapheme cluster is split between the start and end of the string. For example, rotating "abcdéfghijk" by 5 will result in "efghijkabcd\u{301}", with the acute accent stranded on its own, away from the 'e'.

If your strings are ASCII then you don't have to worry about that, but then you can also just treat them as byte strings anyway:

#[test]
fn test_rotate_five_ascii() {
    let mut chars = b"I am the string and you should rotate me!   ".clone();
    let res_chars = b"the string and you should rotate me!   I am ";
    rotate_by(&mut chars, 5);
    assert_eq!(chars, &res_chars[..]);
}
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • Thanks a lot. I will check out the implementation of `slice::rotate_left` to see if it uses unsafe operations internally. I guess, if so, one just has to accept that, though I'd be surprised if there was no smart way to do this without going unsafe. – NoBullsh1t Feb 20 '23 at 10:07
  • `rotate_left` is part of the standard library. You shouldn't care if it is implemented with `unsafe` or not. – Peter Hall Feb 20 '23 at 10:38
  • I can kind of see why you would say that. But it makes me wonder: The very first thing `rotate_left` does is `assert!` that the `mid` argument falls within range of the slice. Why would it do that, instead using Rusts beloved error handling strategies? I can see that returning a `Result` isn't easy for an in-place edit, but why not simply make this function return `self` after the edit? If this is a foolish question, could you point me to what to read up on? – NoBullsh1t Feb 20 '23 at 22:30