ExamplesBy LevelBy TopicLearning Paths
951 Intermediate

951 Series Sliding Window

Functional Programming

Tutorial

The Problem

Extract all contiguous substrings of length n from a string using a sliding window. Apply this to find the window with the largest digit product. Implement two variants: the byte-slice .windows(n) approach and an explicit index range version. Also implement a recursive variant that mirrors OCaml's list-based sliding window style.

🎯 Learning Outcomes

  • • Use .as_bytes().windows(n) to produce zero-copy overlapping byte windows over a string slice
  • • Convert byte windows to substrings via std::str::from_utf8
  • • Implement the index-range variant: (0..=len - n).map(|i| &s[i..i+n])
  • • Compute the largest digit product by chaining .windows().map(digit_product).max()
  • • Validate inputs with Result: span too large, invalid (non-digit) characters
  • Code Example

    pub fn series(n: usize, s: &str) -> Vec<String> {
        if n == 0 { return vec![String::new(); s.len() + 1]; }
        s.as_bytes()
            .windows(n)
            .map(|w| std::str::from_utf8(w).unwrap().to_owned())
            .collect()
    }
    
    pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
        if n == 0 { return Ok(1); }
        if n > s.len() { return Err("span too large".to_string()); }
        let max = series(n, s)
            .into_iter()
            .map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
            .max()
            .unwrap_or(0);
        Ok(max)
    }

    Key Differences

    AspectRustOCaml
    Window source.as_bytes().windows(n) — zero-copyString.sub — allocates each substring
    Index iteration(0..=len-n).map(\|i\| ...)List.init (len-n+1) (fun i -> ...)
    Error handlingResult<u64, String> with ?result type
    Product.product::<u64>()List.fold_left ( * ) 1

    .windows(n) is a zero-copy operation — it hands out references into the original byte slice. Converting to String allocates, but the window itself does not. For very large inputs, processing windows without collecting saves memory.

    OCaml Approach

    let series n s =
      let len = String.length s in
      if n = 0 then List.init (len + 1) (fun _ -> "")
      else List.init (len - n + 1) (fun i -> String.sub s i n)
    
    let digit_product sub =
      String.fold_left (fun acc c -> acc * (Char.code c - Char.code '0')) 1 sub
    
    let largest_product n s =
      if n = 0 then Ok 1
      else if n > String.length s then Error "span too large"
      else
        series n s
        |> List.map digit_product
        |> List.fold_left max 0
        |> Result.ok
    

    OCaml's List.init k f generates a list of k elements where element i is f(i). This creates substrings without mutation. String.fold_left computes the digit product purely functionally.

    Full Source

    #![allow(clippy::all)]
    pub fn series(n: usize, s: &str) -> Vec<String> {
        if n == 0 {
            return vec![String::new(); s.len() + 1];
        }
        s.as_bytes()
            .windows(n)
            .map(|w| std::str::from_utf8(w).unwrap().to_owned())
            .collect()
    }
    
    pub fn series_functional(n: usize, s: &str) -> Vec<String> {
        let len = s.len();
        if n == 0 || n > len {
            if n == 0 {
                return vec![String::new(); len + 1];
            }
            return vec![];
        }
        (0..=len - n).map(|i| s[i..i + n].to_owned()).collect()
    }
    
    pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
        if n == 0 {
            return Ok(1);
        }
        if n > s.len() {
            return Err("span too large".to_string());
        }
        if !s.chars().all(|c| c.is_ascii_digit()) {
            return Err("invalid character".to_string());
        }
        let max = series(n, s)
            .into_iter()
            .map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
            .max()
            .unwrap_or(0);
        Ok(max)
    }
    
    pub fn largest_product_recursive(n: usize, s: &str) -> Result<u64, String> {
        if n == 0 {
            return Ok(1);
        }
        if n > s.len() {
            return Err("span too large".to_string());
        }
        fn digit_product(s: &str) -> u64 {
            s.chars().map(|c| c as u64 - '0' as u64).product()
        }
        fn go(n: usize, s: &str, best: u64) -> u64 {
            if s.len() < n {
                best
            } else {
                let p = digit_product(&s[..n]);
                go(n, &s[1..], best.max(p))
            }
        }
        Ok(go(n, s, 0))
    }
    
    /* Output:
       series(3, "49142") = ["491", "914", "142"]
       series_functional(3, "49142") = ["491", "914", "142"]
       largest_product(2, "0123456789") = Ok(72)
       largest_product(6, "49142") = Err("span too large")
       largest_product_recursive(2, "0123456789") = Ok(72)
    */

    Deep Comparison

    OCaml vs Rust: Series — Sliding Window

    Side-by-Side Code

    OCaml

    let series n s =
      if n > String.length s then []
      else
        List.init (String.length s - n + 1) (fun i ->
          String.sub s i n
        )
    
    let largest_product n s =
      if n = 0 then Ok 1
      else if n > String.length s then Error "span too large"
      else
        series n s
        |> List.map (fun sub ->
          String.fold_left (fun acc c ->
            acc * (Char.code c - Char.code '0')
          ) 1 sub
        )
        |> List.fold_left max 0
        |> fun m -> Ok m
    

    Rust (idiomatic)

    pub fn series(n: usize, s: &str) -> Vec<String> {
        if n == 0 { return vec![String::new(); s.len() + 1]; }
        s.as_bytes()
            .windows(n)
            .map(|w| std::str::from_utf8(w).unwrap().to_owned())
            .collect()
    }
    
    pub fn largest_product(n: usize, s: &str) -> Result<u64, String> {
        if n == 0 { return Ok(1); }
        if n > s.len() { return Err("span too large".to_string()); }
        let max = series(n, s)
            .into_iter()
            .map(|sub| sub.chars().map(|c| c as u64 - '0' as u64).product::<u64>())
            .max()
            .unwrap_or(0);
        Ok(max)
    }
    

    Rust (functional/recursive)

    pub fn largest_product_recursive(n: usize, s: &str) -> Result<u64, String> {
        if n == 0 { return Ok(1); }
        if n > s.len() { return Err("span too large".to_string()); }
        fn digit_product(s: &str) -> u64 {
            s.chars().map(|c| c as u64 - '0' as u64).product()
        }
        fn go(n: usize, s: &str, best: u64) -> u64 {
            if s.len() < n { best }
            else {
                let p = digit_product(&s[..n]);
                go(n, &s[1..], best.max(p))
            }
        }
        Ok(go(n, s, 0))
    }
    

    Type Signatures

    ConceptOCamlRust
    Sliding windowsval series : int -> string -> string listfn series(n: usize, s: &str) -> Vec<String>
    Product with errorval largest_product : int -> string -> (int, string) resultfn largest_product(n: usize, s: &str) -> Result<u64, String>
    Fold over charsString.fold_left : ('a -> char -> 'a) -> 'a -> string -> 'a.chars().fold(init, f) or .product()
    Max of listList.fold_left max 0.max().unwrap_or(0)
    Optional integer'a optionOption<T>

    Key Insights

  • **slice::windows() is the idiomatic Rust primitive.** OCaml has no built-in sliding-window for strings; you reconstruct it with List.init + String.sub. Rust's slice::windows(n) is provided by the standard library and produces borrowed sub-slices with zero additional allocation, making it both ergonomic and efficient.
  • Byte slice vs. character slice. Rust strings are UTF-8, so &str has no O(1) indexing. Working via as_bytes() is safe here because ASCII digit strings are single-byte, and from_utf8 re-borrows the window as &str without copying.
  • **product() replaces fold_left (*) 1.** OCaml's String.fold_left (fun acc c -> acc * digit_of c) 1 sub maps directly to .chars().map(digit_of).product::<u64>() in Rust. The product iterator method is the exact Rust idiom for multiplicative folds.
  • Explicit integer width matters. OCaml integers are arbitrary precision (via the runtime); Rust requires you to pick a type. u64 safely holds the product of up to 19 single-digit values before overflow, which covers all practical inputs for this problem.
  • Recursive tail call with accumulator. The recursive Rust version passes a best accumulator through go(n, &s[1..], best.max(p)), consuming one character per recursive call — exactly the OCaml List.fold_left pattern translated to explicit recursion on string slices.
  • When to Use Each Style

    Use idiomatic Rust when: you want readable, allocation-light code that composes naturally with the rest of the iterator pipeline. windows() + map() + max() reads as a single declarative query.

    Use recursive Rust when: teaching the OCaml parallel explicitly, or when the algorithm is naturally expressed as "process head, recurse on tail" and you want to avoid collecting an intermediate Vec.

    Exercises

  • Implement max_window_sum(n, nums: &[i64]) — the maximum sum of any contiguous subarray of length n.
  • Implement distinct_windows(n, s) — count the number of distinct substrings of length n.
  • Add validation that the span is non-zero in series and return an empty Vec rather than the current n+1 empty strings.
  • Implement a streaming version that processes windows one at a time without collecting the full Vec<String>.
  • Extend largest_product to return the starting index of the maximum window in addition to the product value.
  • Open Source Repos