::  to use, download UnicdoeData.txt and place it in `%/lib/unicode-data/txt`.
::
::::
::
::  part 1: parse the file into {uppers}
::
/-  unicode-data
/+  new-hoon
/=  case-table
  /;  |=  a=(list line:unicode-data)
      =,  new-hoon
      |^  %-  build-tree
          %-  flop
          (build-case-nodes a)
      ::
      :>  #
      :>  #  %case-nodes
      :>  #
      :>    transforms raw unicode data into sequential case nodes.
      +|
      ++  build-case-nodes
        :>  raw list of unicode data lines to a compact list of chardata
        |=  a=(list line:unicode-data)
        ^-  (list case-node:unicode-data)
        =<  out
        ::
        ::  todo: we don't have the final case range in the output of this
        ::  gate. this is because this algorithm doesn't work when the last
        ::  char is part of a range. this doesn't happen with the real one,
        ::  only the excerpts i was using for testing.
        ::
        %^  foldl:ls  a  *case-fold
        |=  [c=case-fold l=line:unicode-data]
        ^+  c
        =+  state=(line-to-case-state l)
        ?:  (is-adjacent state prev.c)
          c(prev state)
        =.  c  (add-range c)
        %=  c
          start
            ?:  &(!=(case.state %missing) !=(case.state %none))
              `state
            ~
          prev  state
        ==
      ::
      ++  line-to-case-state
        :>  creates an easy to merge form.
        |=  line:unicode-data
        ^-  case-state
        =/  out=case-state
          [code %none [%none ~] [%none ~] [%none ~]]
        ?:  =(code `@c`0)
          =.  case.out  %missing
          out
        =.  case.out
          ?+  gen  %none
            $lu  %upper
            $ll  %lower
            $lt  %title
          ==
        ::
        ::  several characters aren't described as $lu or $ll but have lower or
        ::  upper state, such as u+2161. detect this and fix it up.
        ::
        =?  case.out  &(=(case.out %none) !=(low ~))  %upper
        =?  case.out  &(=(case.out %none) !=(up ~))  %lower
        ::
        ::  calculate offsets
        ::
        =?  upper.out  !=(up ~)     (calculate-offset (need up) code)
        =?  lower.out  !=(low ~)
           (calculate-offset (need low) code)
        =?  title.out  !=(title ~)  (calculate-offset (need title) code)
        out
      ::
      ++  calculate-offset
        |=  [src=@c dst=@c]
        ^-  case-offset:unicode-data
        ?:  =(src dst)
          [%none ~]
        ?:  (gth src dst)
          [%add (sub src dst)]
        [%sub (sub dst src)]
      ::
      ++  is-adjacent
        :>  is {rhs} a continuation of {lhs}?
        |=  [lhs=case-state rhs=case-state]
        ^-  ?
        ?:  (lth point.rhs point.lhs)
          $(lhs rhs, rhs lhs)
        ?:  !=(point.rhs +(point.lhs))
          %.n
        ?:  !=(case.rhs case.lhs)
          (upper-lower-adjacent lhs rhs)
        ?:  =(case.lhs %none)
          %.n
        ?:  =(case.lhs %missing)
          %.n
        ?:  !=(upper.lhs upper.rhs)
          %.n
        ?:  !=(lower.lhs lower.rhs)
          %.n
        ?:  !=(title.lhs title.rhs)
          %.n
        %.y
      ::
      ++  upper-lower-adjacent
        :>    detects %upper-lower spans.
        :>
        :>  is {lhs} the same as {rhs}, but with opposite case?
        |=  [lhs=case-state rhs=case-state]
        ?:  &(=(case.lhs %upper) !=(case.rhs %lower))
          %.n
        ?:  &(=(case.lhs %lower) !=(case.rhs %upper))
          %.n
        ::
        ::  to simplify detection, if things are in the opposite order, redo
        ::  things flipped.
        ::
        ?:  =(case.lhs %lower)
          $(lhs rhs, rhs lhs)
        ?&  (is-upper-lower lhs)
            (is-lower-upper rhs)
        ==
      ::
      ++  is-upper-lower
        |=  i=case-state
        =(+.+.i [[%none ~] [%add 1] [%none ~]])
      ::
      ++  is-lower-upper
        |=  i=case-state
        =(+.+.i [[%sub 1] [%none ~] [%sub 1]])
      ::
      ++  is-none
        |=  i=case-state
        =(+.+.i [[%none ~] [%none ~] [%none ~]])
      ::
      ++  add-range
        |=  c=case-fold
        ^+  c
        ?~  start.c
          c
        ?:  (is-none u.start.c)
          c
        ?:  ?&  (gth point.prev.c point.u.start.c)
                (is-upper-lower u.start.c)
            ==
          =/  node=case-node:unicode-data
            [`@ux`point.u.start.c `@ux`point.prev.c [%uplo ~] [%uplo ~] [%uplo ~]]
          c(out [node out.c])
        =/  node=case-node:unicode-data
          [`@ux`point.u.start.c `@ux`point.prev.c +.+.u.start.c]
        c(out [node out.c])
      ::
      ++  case-fold
        :>  state that's part of the fold which generates the list of case-nodes
        $:  :>  resulting data to pass to treeify.
            out=(list case-node:unicode-data)
            :>  the start of a run of characters; ~ for not active.
            start=(unit case-state)
            :>  previous character state
            prev=case-state
        ==
      ::
      ++  case-state
        :>  a temporary model which we compress later in a second pass.
        $:  point=@c
            case=case-class
            upper=case-offset:unicode-data
            lower=case-offset:unicode-data
            title=case-offset:unicode-data
        ==
      ::
      ++  case-class
        :>  classification of an individual character.
        $?  $upper
            $lower
            $title
            $none
            $missing
        ==
      ::
      :>  #
      :>  #  %tree-building
      :>  #
      :>    builds a binary search tree out of the list
      +|
      ++  build-tree
        |=  a=(list case-node:unicode-data)
        ^-  case-tree:unicode-data
        ::  there's probably a bottom up approach that doesn't require walking
        ::  a list over and over again.
        ?~  a
          ~
        =+  len=(lent a)
        =+  [lhs rhs]=(split-at:ls (div len 2) a)
        ?~  rhs
          ?~  lhs
            ~
          [i.lhs ~ ~]
        =+  x=[i.rhs $(a lhs) $(a t.rhs)]
        x
      --
  /:  /===/lib/unicode-data  /&unicode-data&/txt/
::
::  part 2: utility core
::
|%
++  transform
  |=  [a=tape fun=$-(@c @c)]
  %-  tufa
  (turn (tuba a) fun)
::
++  to-upper
  :>  returns the uppercase of unicode codepoint {a}
  |=  a=@c
  ^-  @c
  ::  special case ascii to not perform map lookup.
  ?:  (lte a max-ascii)
    ?:  &((gte a 'a') (lte a 'z'))
      (sub a 32)
    a
  (apply-table a case-table %upper)
::
++  to-lower
  :>  returns the lowercase of unicode codepoint {a}
  |=  a=@c
  ^-  @c
  ?:  (lte a max-ascii)
    ?:  &((gte a 'A') (lte a 'Z'))
      (add 32 a)
    a
  (apply-table a case-table %lower)
::
++  apply-table
  :>    searches {table} and apples applies {type} to {a}.
  :>
  :>  this recursively walks the case tree {table}. if it finds an entry which
  :>  matches on {a}, it will apply the offset. otherwise, returns {a}.
  |=  [a=@c table=case-tree:unicode-data type=?($upper $lower $title)]
  ^-  @c
  ?~  table
    a
  ?:  (lth a start.n.table)
    $(table l.table)
  ?:  (gth a end.n.table)
    $(table r.table)
  ?.  &((lte start.n.table a) (lte a end.n.table))
    a
  %^  apply-offset  a  type
  ?-  type
    $upper  upper.n.table
    $lower  lower.n.table
    $title  title.n.table
  ==
::
++  apply-offset
  :>  applies an character offset to {a}.
  |=  [a=@c type=?($upper $lower $title) offset=case-offset:unicode-data]
  ^-  @c
  ?-  offset
    {$add *}   (add a a.offset)
    {$sub *}   (sub a s.offset)
    {$none *}  a
  ::
      {$uplo *}
    ?-  type
      $upper  (sub a 1)
      $lower  (add a 1)
      $title  (sub a 1)
    ==
  ==
::
++  max-ascii  `@c`0x7f
--
::
::  part 3: generator
::
:-  %say
|=  $:  [now=@da eny=@uvJ bec=beak]
        [n=tape $~]
        $~
    ==
:-  %tape  (transform n to-upper)