iyi

Prelude, 0.18.0

String

Text — SPEC.md, "0.1.0", item 3.

In the prelude: every iyi program has it, with no import.

The compiler declares String's fields itself — @bytesize, @length and @c, in that order, in Program#initialize — because codegen lays out string literals before any prelude is read. So this file reopens a type that already has its shape and only says what can be done with it.

Stringclass

Stringiyi doc String
class String

  # `"ab" * -3` answered `""` where the other library raises (III.1.7a).
  # The size is a division, so the refusal names the string: `"abcd" *
  # 600_000_000` panicked "arithmetic overflow", the machine's adder.
  def *(count : Int32) : String

  def +(other : String) : String

  # Byte order, which for ASCII is dictionary order with the capitals first;
  # a prefix sorts before what it prefixes. What `sorted` and `sorted_by`
  # ask of a string.
  def <(other : String) : Bool

  # Thirty-two bytes a step while they last, then one at a time, and the
  # same string is itself: a byte a step was eleven times Python's `==`,
  # 230 ms for 1,000 compares of equal megabytes. No load needs alignment.
  def ==(other : String) : Bool

  # A negative *start* counts from the end, the same wrap as the one-index
  # form. *count* stays non-negative: a negative count is not a wrap, it is a
  # mistake, and it raises for that reason rather than being silently clamped.
  def [](start : Int32, count : Int32) : String

  # The character at *index*, asked for by `samples/iyi/calc`, which scans
  # its input a character at a time. Byte-indexed: a byte is the character
  # for ASCII, and `each_char` is the decoded walk. A negative index counts
  # from the end; out of range after that wrap raises, `[]?` answers nil.
  def [](index : Int32) : Char

  def []?(index : Int32) : Char | ::Nil

  # Where *search* first starts at or after byte *offset* - counted from
  # the end when negative - in bytes, as Crystal's `byte_index` answers.
  # Naive while that is cheap, and Rabin-Karp - one rolling hash per
  # position, bytes compared only where it matches - once more than four
  # bytes a position have been compared: a needle that keeps almost
  # matching, `"a" * 1000 + "b"` in a run of `a`, made the naive loop
  # n * m, 222 ms for a megabyte.
  def byte_index(search : String, offset : Int32 = 0) : Int32 | ::Nil

  def bytesize : Int32

  # `'\n'` is a line's end, so it takes the `\r` before it, or a lone `\r`
  # in its place, as the other library's does: `"a\r\n".chomp('\n')` is "a".
  def chomp(char : Char) : String

  # The other half of `upcase`, ASCII for the same reason.
  def downcase : String

  # Every character in order, decoded, as many as `size` counts: it yielded
  # one per byte, so `"héllo".size` was 5 and `each_char` ran six times.
  # Indexing stays byte-indexed (`[]` above), as `samples/iyi/calc` scans.
  # A byte that begins no character is one U+FFFD, as in the other library:
  # a lead byte took the bytes after it whatever they were, C3 41 was the
  # one character U+00C1, and `"a" C3 '"'` swallowed the quote.
  def each_char(& : (Char -> Nil)) : Nil

  def empty? : Bool

  def ends_with?(suffix : String) : Bool

  # FNV-1a over the bytes: three lines, no table, and good enough for a
  # linear-probing table of a few dozen entries.
  def hash : Int32

  def includes?(other : String) : Bool

  # Where a character first is, or nil: the question a parser asks before
  # it cuts.
  def index(char : Char) : Int32 | ::Nil

  # The literal that spells it, escaped as the other library's, a byte that
  # is no UTF-8 as `\x`: unescaped, `["a\", \"b"]` printed as two strings.
  def inspect : String

  def lchop(char : Char) : String

  # The lines of a text, blank ones included, a final newline ending the last
  # line; `\r\n` ends one too, and a last `\r` with no `\n` after it is kept:
  # `"1\r\n".lines[0].to_i` panicked where `File.read_lines` read 1.
  def lines : Array(String)

  def ljust(width : Int32, pad : Char = ' ') : String

  # Pad on the left (rjust) or the right (ljust) to *width*, in characters
  # as `size` counts them, the pad as its own bytes: in bytes,
  # `"héllo".rjust(7, '.')` answered a string of `size` 6, and a `'·'`
  # written as one byte left a lone continuation byte in the result.
  def rjust(width : Int32, pad : Char = ' ') : String

  # A zero `@length` on a non-empty string is "not counted yet", which is
  # how the other library's own code leaves a string it builds: `to_json`'s
  # result answered `size` 0 here while the field was returned as it stood.
  # The layout is shared on purpose (SPEC.md Part V item 12), so the
  # invariant is too. The count is of what `each_char` yields, a byte that
  # begins no character one U+FFFD.
  def size : Int32

  # The pieces between separators, empty ones included: `"a,,b".split(',')`
  # is three, `"a,".split(',')` is two, and `"".split(',')` is one empty
  # piece - the count the other library gives, so a program reads the same
  # under both.
  def split(separator : Char) : Array(String)

  # The three the Kemal port's path joining needs, byte-indexed: the same
  # as by character while the strings are ASCII.
  def starts_with?(prefix : String) : Bool

  # A `Char` above ASCII is more than one byte, and these narrowed it to one
  # through a *checked* convert, so `"a☃b".index('☃')` died of "arithmetic
  # overflow". Each compares the character's own bytes now.
  def starts_with?(char : Char) : Bool

  # Without `Char#whitespace?`'s characters at either end, decoded: it read
  # bytes, and `"b\u00A0".strip` kept the space the other library takes.
  def strip : String

  # The double nearest the decimal written here, correctly rounded: the
  # digits and the exponent are read into the printer's bignum and the
  # quotient taken to 54 bits with a sticky remainder, so what `to_s`
  # wrote reads back as the same double, every time, and "0.1" is the
  # double the literal `0.1` is. Accepts `-1.5`, `2.5e-3`, `1E10`, `.5`,
  # `7.`, `Infinity`, `NaN`, with `Char#whitespace?` around as `to_i?`
  # takes it; raises "not a number" for anything else.
  def to_f : Float64

  # The number this string spells - an optional sign and decimal digits,
  # the whole string - as `samples/iyi/calc` reads a token. Raises on
  # anything else, as the other library's does (SPEC.md III.1.7a): a
  # silent 0 for `"oops"` is wrong twice; `to_i?` answers nil for it.
  def to_i : Int32

  # Around the digits, `Char#whitespace?` is skipped as the other
  # library's `whitespace: true` skips it (`"42\n".to_i?` was nil). Read
  # as a *negative* magnitude, which holds every value the type has:
  # `2147483648` accumulated and negated, `Int32::MIN` overflowed on its
  # last digit, a panic rather than the `nil` a `?` promises.
  def to_i? : Int32 | ::Nil

  def to_s : String

  def to_unsafe : Pointer(UInt8)

  # ASCII only, and the samples are ASCII. A prelude that folded case for
  # every alphabet would be a Unicode table, which is more lines than this
  # whole file and not what any sample is about.
  def upcase : String

  def self.build(capacity : Int32 = 64, & : (String::Builder -> _)) : String

  # Interpolation is a call the compiler writes: `"a#{b}"` reaches here as
  # `String.interpolation("a", b)` (`LiteralExpander#expand`). The overloads
  # are what let the common shapes avoid work — a single string is itself.
  def self.interpolation(value : String) : String

  def self.interpolation(value) : String

  def self.interpolation(*values : *T) : String forall T

  # Allocates *bytesize* bytes, hands the buffer to the block to fill, and
  # returns the string that results. The character count is left at zero,
  # "not counted yet", for `size` to scan the first time it is asked: most
  # strings built here - an encoding, a join, a line - are never asked, and
  # counting them all was a second pass over every byte built.
  #
  # The byte after the last is a NUL, as a literal's is, so `to_unsafe` is
  # a C string: `std/process` and `std/debug` hand one to the platform as a
  # path. The byte was allocated for that and never written, and a string's
  # chunk is atomic - not cleared - so a chunk that had held a longer
  # string kept that string's byte there. Measured on Windows after a
  # collection that freed longer strings: 961 of 1,000 strings built after
  # it had no terminator, and `Process.run` of a path built that way
  # answered "no such program" for a file that was there.
  def self.new(bytesize : Int32, & : (Pointer(UInt8) -> Nil)) : String
end