\ ratio.4th --- written by Hugh Aguilar --- copyright (c) 2010, BSD license
\ Compiles under any ANS-Forth system (16-bit, 32-bit or 64-bit).
\ This is an implementation of lists of rational numbers
\ See the file RATIO.TXT for further documentation.
\ Author contact: hughaguilar96@yahoo.com

\ We need NOVICE.4TH and LIST.4TH already compiled.


marker ratio.4th

list
    d field .num    \ signed numerator          the sign of the numerator is the sign of the ratio
    w field .den    \ unsigned denominator
constant ratio

\ The denominator can be zero, which indicates infinity; the sign of the numerator is still valid.
\ The numerator is a double to help support ratios larger than one.

: <<simplify>> { Lnum Hnum Lden Hden | sgn whole divisor -- Dnum den }
    Hnum 0< to sgn
    Lnum Hnum dabs  to Hnum to Lnum
    Lnum Hnum Lden Hden dgcd  abort" *** <<SIMPLIFY>> overflow in DGCD ***"
    begin  dup even? while                          \ halve everything until divisor is odd
        Lnum Hnum d2/  to Hnum to Lnum
        Lden Hden d2/  to Hden to Lden
        2/ repeat  to divisor
    divisor 1 = if                                  \ if divisor is 1, we are done
        Hden abort" *** <<SIMPLIFY>> overflow in denominator***"
        Lnum Hnum  sgn if  dnegate  then
        Lden  exit then
    Hden 0= if                                      \ if denominator is a single, reduce numerator
        Lnum Hnum  Lden um/mod  to whole            \ numerator is now single-precision
        to Lnum  0 to Hnum  then
    Lden Hden  divisor um/mod nip  to Lden          \ Hden is no longer valid
    Lnum Hnum  divisor um/mod nip  u>d
    whole Lden um*  d+  sgn if  dnegate  then
    Lden ;

\ <<SIMPLIFY>> first halves everything until the divisor is odd.
\ This might convert the denominator from a double to a single, which will help.
\ If the divisor is 1, then we are done; we can't simplify any further.
\ If the denominator is a single, the numerator is converted into a single.
\ This prevents overflow in the case that the divisor is small.
\ Finally both the numerator and denominator are divided by the divisor.

: <simplify> { node -- }
    node .num 2@ d0= if     exit then       \ ignore zero
    node .den @ 0= if       exit then       \ ignore infinite
    node .num 2@  node .den @ u>d  <<simplify>>  node .den !  node .num 2! ;

: simplify ( head -- )
    ['] <simplify> each ;

: init-ratio ( Dnum den node -- node )
    init-list >r
    dup 0< abort" *** INIT-RATIO doesn't allow negative denominators ***"
    r@ .den !
    r@ .num 2!
    r@ <simplify>
    r> ;

: new-ratio ( Dnum den -- node )
    ratio alloc
    init-ratio ;

: <kill-ratio> ( node -- )
    dealloc ;
    
: kill-ratio ( head -- )
    each[  <kill-ratio>  ]each ;
    
: add-int { n node | sgn -- }
    n 0< to sgn
    node .den @  n um*  sgn if  dnegate  then
    node .num 2@  d+  node .num 2! ;

min-signed constant |                               \ the bad number used as a bracket

\ The MIN-SIGNED value hopefully won't be used by accident, but this would cause a bug.

: make-ratio ( bad n... bad -- head )               \ fills numerators, sets denominators to 1
    >r                                                          \ r: -- bad
    nil begin  over r@ <> while             \ -- bad n... head
        swap s>d 1 new-ratio  swap link  repeat
    swap r> 2drop ;                                             \ throw away the two bads

\ MAKE-RATIO only accepts single-precision numerators.
\ Our .NUM field is double-precision in order to support ratios greater than one.

: set-dens ( bad n... bad head -- new-head )        \ sets denominators
    reverse  dup >r
    swap >r                                                     \ r: -- head bad
    begin  over r@ <> while
        dup 0= abort" *** SET-DENS had too many parameters ***"
        over 0< abort" *** SET-DENS doesn't allow negative denominators ***"
        tuck .den !  .fore @  repeat drop                       \ throw away the head (NIL by now)
    swap r> 2drop                                               \ throw away the two bads
    r> reverse
    dup simplify ;

\ SET-DENS disallows negative numbers because this is most likely a mistake.
\ It is unlikely that the user was entering a gigantic unsigned number.

: add-ints ( bad n... bad head -- new-head )        \ adds whole numbers to numerators
    reverse  dup >r
    swap >r                                                     \ r: -- head bad
    begin  over r@ <> while
        dup 0= abort" *** ADD-INTS had too many parameters ***"
        tuck add-int  .fore @  repeat drop                      \ throw away the head (NIL by now)
    swap r> 2drop                                               \ throw away the two bads
    r> reverse
    dup simplify ;

: <u.> ( u -- )                             \ like U. except with no space afterward
    u>d <# #s #> type ;

: <show-ratio> { node -- }
    node .den @ 0= if       node .num @ 0< if  ." -inf "  else  ." +inf "  then     exit then
    node .num 2@ d0= if     0 .                                                     exit then
    node .num 2@ d0< if     [char] - emit       then
    node .num 2@ dabs  node .den @  um/mod
    ?dup if  <u.>
        dup if
            node .num 2@ d0< if  [char] -  else  [char] +  then  emit
            then
        then
    ?dup if     <u.>  [char] / emit  node .den @ u.
    else        bl emit                                 then ;

: show-ratio ( head -- )
    ." | "
    ['] <show-ratio>  each
    ." | " ;

: <ratio-negate> { node -- }
    node .num 2@  dnegate  node .num 2! ;

: ratio-negate ( head -- )          \ negate all of the nodes in head-list
    ['] <ratio-negate> each ;

: <ratio-invert> { node | sgn -- }
    node .num 2@  dup 0< to sgn  dabs
    abort" *** <RATIO-INVERT> overflow ***"         \ -- new-den
    node .den @ u>d  sgn if  dnegate  then
    node .num 2!
    node .den ! ;

: ratio-invert ( head -- )          \ invert all of the nodes in head-list
    ['] <ratio-invert> each ;

100000000.  2constant ratio-scalar

\ RATIO-SCALAR is small so that the ratio can later on be multiplied by another ratio
\ and it hopefully won't overflow. Overflow is the big problem with these ratios.

: <ratio-sqrt> { node -- }
    node .num 2@ d0< abort" *** RATIO-SQRT can't accept negative arguments ***"
    node .den @ 0= if  exit then
    ratio-scalar  node .den @  um/mod nip  dup um*
    2dup  node .num 2@  d*  dsqrt u>d   node .num 2!
    node .den @  du*        dsqrt       node .den !
    node <simplify> ;

: ratio-sqrt ( head -- )            \ square-root all of the nodes in head-list
    ['] <ratio-sqrt> each ;

: ratio>float { node -- float }
    node .num 2@ d>f
    node .den @  dup 0= abort" *** RATIO>FLOAT divide by zero ***"
    s>d d>f f/ ;

: float>ratio ( float -- node )
    ratio-scalar d>f  f* f>d
    ratio-scalar drop  new-ratio ;

: <ratio-sq> { node -- }
    node .den @ 0= if  1. node .num 2!  exit then   \ infinites become positive when squared
    node .num 2@    2dup d*
    node .den @     dup um*                         \ -- Dnum Dden
    <<simplify>>  node .den !  node .num 2! ;

: ratio-sq ( head -- )              \ square all of the nodes in head-list
    ['] <ratio-sq> each ;

: <multiply-ratio> { dest node | surr -- dest }
    node .den @ 0=  dest .den @ 0=  or if  0 dest .den !
        dest .num 2@ nip  node .num 2@ nip  xor 0< if  -1.  else  1.  then
        dest .num 2!  dest exit then
    node clone-node to surr                         \ use a clone of NODE so we can modify it
    surr .num 2@  dest .num 2@
    surr .num 2!  dest .num 2!                      \ exchange the numerators
    surr <simplify>  dest <simplify>                \ this helps to prevent overflow
    dest .num 2@  surr .num 2@  d*                  \ -- Dnum
    dest .den @   surr .den @   um*                 \ -- Dnum Dden
    <<simplify>>  dest .den !  dest .num 2!
    surr dealloc  dest ;

: product-ratio ( product-node head -- )    \ multiply all of head-list into product-node
    ['] <multiply-ratio> each  drop ;

: <*ratio> ( multiplier node -- multiplier )
    over <multiply-ratio> drop ;

: *ratio ( multiplier head -- )     \ multiply multiplier into all of the nodes in head-list
    ['] <*ratio> each  drop ;

: zero-ratio { node -- }            \ makes node into a zero
    0. node .num 2!
    1  node .den ! ;

: <add-ratio> { dest node | dest-mult -- dest }         \ adds node to dest
    node .den @ 0= if
        dest .den @ 0= if
            node .num 2@ nip  dest .num 2@ nip  xor 0< if  dest zero-ratio  then
            exit then
        node .num @  dest .num !
        node .den @  dest .den !  exit then
    dest .den @ 0= if  exit then
    node .den @  dest .den @  gcd                       \ -- divisor
    node .den @  over /  to dest-mult
    dest .den @  swap /                                 \ -- Unode-mult
    node .num 2@  rot du*                               \ -- Dnode-num
    dest .num 2@  dest-mult  du*                        \ -- Dnode-num Ddest-num
    d+                                                  \ -- Dnum
    dest .den @  dest-mult um*                          \ -- Dnum Dden
    <<simplify>>  dest .den !  dest .num 2!
    dest ;

: sum-ratio ( sum-node head -- )    \ add all of head-list into the sum-node
    ['] <add-ratio> each  drop ;

: <+ratio> ( addend node -- addend )
    over <add-ratio> drop ;

: +ratio ( addend head -- )         \ add addend to all of the nodes in head-list
    ['] <+ratio> each  drop ;

: ratio-compare { nodeA nodeB -- A>B | A=B | A<B }
    nodeA 0= if
        nodeB 0= if  A=B  exit then
        A<B  exit then
    nodeB 0= if  A>B  exit then
    nodeA .den @ 0= if
        nodeB .den @ 0= if
            nodeA .num 2@ nip  nodeB .num 2@ nip  xor 0< if     \ different signs
                nodeA .num 2@ nip  0< if  A<B  else  A>B  then
                exit then
            A=B  exit then                                      \ same signs
        nodeA .num 2@ nip  0< if  A<B  else  A>B  then
        exit then
    nodeB .den @ 0= if
        nodeB .num 2@ nip  0< if  A>B  else  A<B  then
        exit then
    nodeA .den @  nodeB .den @  gcd                 \ -- divisor
    nodeA .den @  over /                            \ -- divisor multB
    nodeB .den @  rot /                             \ -- multB multA
    nodeA .num 2@  rot du*
    rot  nodeB .num 2@  rot du*                     \ -- DnumA DnumB
    2over 2over  d= if  2drop 2drop  A=B  exit then
    d< if  A<B  else  A>B  then ;

: <ratio-sort> ( new-node node -- new-node ? )
    over ratio-compare  A>B = ;

: ratio-sort ( head -- new-head )
    ['] <ratio-sort> sort-list ;

: mean { head | n result divisor -- result-node }               \ the average of the head-list
    head length to n
    0. 1 new-ratio to result
    1. n new-ratio to divisor
    result head sum-ratio
    result divisor product-ratio
    divisor kill-ratio
    result ;

: <variance> { head | neg-mean surr result -- result-node }     \ the variance numerator
    head mean  dup <ratio-negate>  to neg-mean
    0. 1 new-ratio to result
    head clone-list to surr
    neg-mean surr +ratio  surr ratio-sq
    result surr sum-ratio
    neg-mean kill-ratio  surr kill-ratio
    result ;

: sample-variance { head | mult -- result-node }                \ variance of a sample
    1.  over length 1-  new-ratio to mult
    head <variance>  dup mult product-ratio                     \ -- result-node
    mult kill-ratio ;

: population-variance { head | mult -- result-node }            \ variance of a complete population
    1.  over length  new-ratio to mult
    head <variance>  dup mult product-ratio                     \ -- result-node
    mult kill-ratio ;

: test ( -- head )
    | 200 250 125 | make-ratio >r
    |  15  75  55 | r> set-dens >r
    |   2   0   3 | r> add-ints ;