|
| 1 | +/- |
| 2 | +Copyright (c) 2025 Christopher Bailey. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Christopher Bailey, F. G. Dorais |
| 5 | +-/ |
| 6 | +import Batteries.Data.Char |
| 7 | +import Batteries.Data.Char.AsciiCasing |
| 8 | + |
| 9 | +namespace String |
| 10 | + |
| 11 | +/-- Case folding for ASCII characters only. |
| 12 | +
|
| 13 | +Alphabetic ASCII characters are mapped to their lowercase form, all other characters are left |
| 14 | +unchanged. This agrees with the Unicode case folding algorithm for ASCII characters. |
| 15 | +
|
| 16 | +``` |
| 17 | +#eval "ABC".caseFoldAsciiOnly == "abc" -- true |
| 18 | +#eval "x".caseFoldAsciiOnly == "y" -- false |
| 19 | +#eval "Àà".caseFoldAsciiOnly == "Àà" -- true |
| 20 | +#eval "1$#!".caseFoldAsciiOnly == "1$#!" -- true |
| 21 | +``` |
| 22 | +-/ |
| 23 | +abbrev caseFoldAsciiOnly (s : String) := s.map Char.caseFoldAsciiOnly |
| 24 | + |
| 25 | +-- TODO: Reimplement with finite iterators/streams when available for `String`. |
| 26 | +private partial def beqCaseInsensitiveAsciiOnlyImpl (s₁ s₂ : String) : Bool := |
| 27 | + s₁.length == s₂.length && loop (ToStream.toStream s₁) (ToStream.toStream s₂) |
| 28 | +where |
| 29 | + loop i₁ i₂ := match Stream.next? i₁, Stream.next? i₂ with |
| 30 | + | some (c₁, i₁), some (c₂, i₂) => c₁.beqCaseInsensitiveAsciiOnly c₂ && loop i₁ i₂ |
| 31 | + | none, none => true |
| 32 | + | _, _ => false |
| 33 | + |
| 34 | +/-- |
| 35 | +Bool-valued comparison of two `String`s for *ASCII*-case insensitive equality. |
| 36 | +
|
| 37 | +#eval "abc".beqCaseInsensitiveAsciiOnly "ABC" -- true |
| 38 | +#eval "cba".beqCaseInsensitiveAsciiOnly "ABC" -- false |
| 39 | +#eval "$".beqCaseInsensitiveAsciiOnly "$" -- true |
| 40 | +#eval "a".beqCaseInsensitiveAsciiOnly "b" -- false |
| 41 | +#eval "γ".beqCaseInsensitiveAsciiOnly "Γ" -- false |
| 42 | +-/ |
| 43 | +@[implemented_by beqCaseInsensitiveAsciiOnlyImpl] |
| 44 | +def beqCaseInsensitiveAsciiOnly (s₁ s₂ : String) : Bool := |
| 45 | + s₁.caseFoldAsciiOnly == s₂.caseFoldAsciiOnly |
| 46 | + |
| 47 | +theorem beqCaseInsensitiveAsciiOnly.eqv : Equivalence (beqCaseInsensitiveAsciiOnly · ·) := { |
| 48 | + refl _ := BEq.rfl |
| 49 | + trans _ _ := by simp_all [beqCaseInsensitiveAsciiOnly] |
| 50 | + symm := by simp_all [beqCaseInsensitiveAsciiOnly] |
| 51 | +} |
| 52 | + |
| 53 | +/-- |
| 54 | +Setoid structure on `String` usig `beqCaseInsensitiveAsciiOnly` |
| 55 | +-/ |
| 56 | +def beqCaseInsensitiveAsciiOnly.isSetoid : Setoid String := |
| 57 | + ⟨(beqCaseInsensitiveAsciiOnly · ·), beqCaseInsensitiveAsciiOnly.eqv⟩ |
| 58 | + |
| 59 | +-- TODO: Reimplement with finite iterators/streams when available for `String`. |
| 60 | +private partial def cmpCaseInsensitiveAsciiOnlyImpl (s₁ s₂ : String) : Ordering := |
| 61 | + loop (ToStream.toStream s₁) (ToStream.toStream s₂) |
| 62 | +where |
| 63 | + loop i₁ i₂ := match Stream.next? i₁, Stream.next? i₂ with |
| 64 | + | some (c₁, i₁), some (c₂, i₂) => c₁.cmpCaseInsensitiveAsciiOnly c₂ |>.then (loop i₁ i₂) |
| 65 | + | some _, none => .gt |
| 66 | + | none, some _ => .lt |
| 67 | + | none, none => .eq |
| 68 | + |
| 69 | +/-- |
| 70 | +ASCII-case insensitive implementation comparison returning an `Ordering`. Useful for sorting. |
| 71 | +
|
| 72 | +``` |
| 73 | +#eval cmpCaseInsensitiveAsciiOnly "a" "A" -- eq |
| 74 | +#eval cmpCaseInsensitiveAsciiOnly "a" "a" -- eq |
| 75 | +#eval cmpCaseInsensitiveAsciiOnly "$" "$" -- eq |
| 76 | +#eval cmpCaseInsensitiveAsciiOnly "a" "b" -- lt |
| 77 | +#eval cmpCaseInsensitiveAsciiOnly "γ" "Γ" -- gt |
| 78 | +``` |
| 79 | +-/ |
| 80 | +@[implemented_by cmpCaseInsensitiveAsciiOnlyImpl] |
| 81 | +def cmpCaseInsensitiveAsciiOnly (s₁ s₂ : String) : Ordering := |
| 82 | + compare s₁.caseFoldAsciiOnly s₂.caseFoldAsciiOnly |
0 commit comments