/* decimalFormat.cls */
::class 'decimalFormat' public

::method init
    expose mask groupingSize decimalSeparator groupingSeparator
    use strict arg mask = '#,###.##'

-- Be sure ooRexx version is dated 20071030 or higher
    parse version ooRexxVer
    if date('s',ooRexxVer~subWord(3),'n') < 20071030 then
        raise syntax 3.903 array('decimalFormat.cls')

    if mask = '' then
        mask = '#,###.##'
    self~validateMask

    groupingSize = 3
    decimalSeparator  = '.'
    groupingSeparator = ','

::method getVersion
return 'Beta .4.1'
/*
Beta        11/15/07 AM
            Lee's original version sent only to Rick

Beta .1     11/15/07 PM
            Incorporated changes suggested by Rick, attributes vs. methods,
            Use verify to examine the parts of the mask
            Broke separators into decimalSeparator & groupingSeparator

Beta .2     11/16/07
            Combined verification of positive pattern & negative pattern into one method and preserved
            applicable error messages

            Added ability to get/set the following attributes
                Positive prefix  - pPrefix
                Positive pattern - pPattern
                Positive suffix  - pSuffix

                Negative prefix  - nPrefix
                Negative pattern - nPattern
                Negative suffix  - nSuffix

                Zero pattern     - zPattern

        The next 2 items may can be improved, but are sufficient for now.
            Added ability to retrieve grouping setting for either positive, negative, or both patterns
            Added ability to set grouping for either positive, negative, or both patterns

        Changed the way numeric digits is set from an arbitrary value of 30
        to a computed value based on the length of the input + the number of decimal places
        specified in the pattern.

Beta .3     11/17/07
        Created single method (buildMask) for duplicated code in the following
            pPrefix
            pPattern
            pSuffix

            nPrefix
            nPattern
            nSuffix

            zPattern


Beta .3.1   11/18/07
        Move check for decimalSeparator/groupingSeparator to just before they are applied.
        Ascertain that a comma does not follow a decimal in mask portion of patterns

Beta .3.2   11/19/07
        Fixed problem is setGrouping
        Disallowed .nil as a zero pattern

Beta .4     11/20/07
        Renamed the following to ease confusion
            pPattern    ->  pMask
            nPattern    ->  nMask
            zPattern    ->  zMask

        Removed getGrouping & setGrouping
            Added pGrouping & nGrouping

        Documentation brought up to date

Beta .4.1   11/20/07
        0 values should now have proper formatting
*/

-- get/set positive grouping
::attribute pGrouping get
::attribute pGrouping set
    expose pGrouping pMask
    use strict arg new_grouping
    if \new_grouping~datatype('O') then
        raise syntax 93.900 array('Positive Grouping Indicator Must Be A Logical Value')
    select
        when new_grouping & pGrouping then
            nop     -- Already .true
        when \new_grouping & \pGrouping then
            nop     -- Already .false
        when new_grouping then
            pMask = ','||pMask
        when \new_grouping then
            pMask = pMask~changeStr(',','')
        otherwise
            nop
    end
    self~buildMask
return 0

-- get/set negative grouping
::attribute nGrouping get
::attribute nGrouping set
    expose nGrouping nMask
    use strict arg new_grouping
    if \new_grouping~datatype('O') then
        raise syntax 93.900 array('Negative Grouping Indicator Must Be A Logical Value')
    select
        when new_grouping & nGrouping then
            nop     -- Already .true
        when \new_grouping & \nGrouping then
            nop     -- Already .false
        when new_grouping then
            nMask = ','||nMask
        when \new_grouping then
            nMask = nMask~changeStr(',','')
        otherwise
            nop
    end
    self~buildMask
return 0

-- get/set postive prefix
::attribute pPrefix get
::attribute pPrefix set
    expose pPrefix
    use strict arg new_pattern
    pPrefix = new_pattern
    self~buildMask
return 0

-- get/set positive pattern
::attribute pMask get
::attribute pMask set
    expose pMask
    use strict arg new_pattern
    pMask = new_pattern
    self~buildMask
return 0

-- get/set positive suffix
::attribute pSuffix get
::attribute pSuffix set
    expose pSuffix
    use strict arg new_pattern
    pSuffix = new_pattern
    self~buildMask
return 0

-- get/set negative prefix
::attribute nPrefix get
::attribute nPrefix set
    expose nPrefix
    use strict arg new_pattern
    nPrefix = new_pattern
    self~buildMask
return 0

-- get/set negative pattern
::attribute nMask get
::attribute nMask set
    expose nMask
    use strict arg new_pattern
    nMask = new_pattern
    self~buildMask
return 0

-- get/set negative suffix
::attribute nSuffix get
::attribute nSuffix set
    expose nSuffix
    use strict arg new_pattern
    nSuffix = new_pattern
    self~buildMask
return 0

::attribute zMask get
::attribute zMask set
    expose zMask have_zMask
    use strict arg new_pattern
    zMask = new_pattern
    have_zMask = .true
    if zMask == '' then
        have_zMask = .false
    self~buildMask
return 0

::method buildMask
    expose mask pPrefix pMask pSuffix nPrefix nMask nSuffix zMask have_zMask
    mask = ''
    if pPrefix \= '' then
        mask = '"'pPrefix'"'
    mask = mask||pMask
    if pSuffix \= '' then
        mask = mask||'"'pSuffix'"'
    mask = mask';'
    if nPrefix \= '' then
        mask = mask'"'nPrefix'"'
    mask = mask||nMask
    if nSuffix \= '' then
        mask = mask'"'nSuffix'"'
    if have_zMask then
        do
            mask = mask';'zMask
        end
    self~pattern = mask
return 0

-- get/set grouping size
::attribute groupingSize get
::attribute groupingSize set
    expose groupingSize
    use strict arg new_groupingSize
    .argUtil~validatePositive("grouping size", new_groupingSize)
    groupingSize = new_groupingSize
return 0

-- get/set decimalSeparator
::attribute decimalSeparator get
::attribute decimalSeparator set
    expose decimalSeparator
    use strict arg new_separator
    if new_separator~length <> 1 then
        raise syntax 93.900 array('Decimal Separator Must Have A Length of 1')
    decimalSeparator = new_separator
return 0

-- get/set groupingSeparator
::attribute groupingSeparator get
::attribute groupingSeparator set
    expose groupingSeparator
    use strict arg new_separator
    if new_separator~length <> 1 then
        raise syntax 93.900 array('Grouping Separator Must Have A Length of 1')
   groupingSeparator = new_separator
return 0

-- get the complete current pattern
::attribute pattern get
    expose mask
return mask

-- set the complete pattern
::attribute pattern set
    expose mask
    use strict arg mask
    self~validateMask
return 0

-- This method is for internal use only
::method validateMask
    expose mask pPrefix pMask pSuffix nPrefix nMask nSuffix zero_prefix zMask zero_suffix have_zMask the_mask the_prefix the_suffix pGrouping nGrouping

-- see if there's a ; divider, if so can't be > 2
    if mask~countStr(';') > 2 then
        raise syntax 93.900 array('Pattern Can NOT Have More The 2 Semi-Colons')

-- break down the complete pattern into its parts
    parse var mask WpMask';'WnMask';'WzMask

    if WzMask \= '' then
        have_zMask = .true
    else
        have_zMask = .false

-- examine the positive pattern
    self~examineMask(WpMask,'P')
-- load the postive pattern parts
    pMask = the_mask
    pPrefix = the_prefix
    pSuffix = the_suffix

-- if no negative pattern specified, use the same positive pattern
    if WnMask = '' then
        do
            nPrefix   = '-'pPrefix
            nMask     = pMask
            nSuffix   = pSuffix
            nGrouping = pGrouping
        end
    else
-- examine the negative pattern
        do
            self~examineMask(WnMask,'N')
-- load the negative pattern parts
            if the_mask = '' then
                nMask = pMask
            else
                nMask = the_mask

            if the_prefix = '' then
                nPrefix = pPrefix
            else
                nPrefix = the_prefix

            if the_suffix = '' then
                nSuffix = pSuffix
            else
                nSuffix = the_suffix
        end

-----------------------------------------------------------------------------------
-- examine all the pieces of the zMask

    if WzMask == 'The NIL object' then
        raise syntax 93.900 array('The Zero Pattern Can Not Be Set To .nil')

    if WzMask == '' then
        do
-- use the pos variables if no zero mask is specified
            zero_prefix = pPrefix
            zMask   = pMask
            zero_suffix = pSuffix
            have_zMask = .false
        end
    else
        do
-- ascertain that single and/or double quotes are paired
            have_zMask = .true
            if WzMask~pos("'") > 0 then
                do
                    if WzMask~countStr("'") // 2 \= 0 then
                        raise syntax 93.900 array('Single Quotes Must Be Matched In The Zero Pattern')
                end

            if WzMask~pos('"') > 0 then
                do
                    if WzMask~countStr('"') // 2 \= 0 then
                        raise syntax 93.900 array('Double Quotes Must Be Matched In The Zero Pattern')
                end
-- set up used variables and strip the quotes
            zero_prefix = ''
            zMask   = WzMask~strip('b',"'")
            zMask   = zMask~strip('b','"')
            zero_suffix = ''
        end

-- This method is for internal use only
::method examineMask
    expose the_mask the_prefix the_suffix pGrouping nGrouping have_zMask
    use strict arg the_mask,np
    np = np~translate

-- will be either the positive or negative pattern
    the_mask = the_mask~strip

-- be sure single quotes are paired
    if the_mask~pos("'") > 0 then
        do
            if the_mask~countStr("'") // 2 \= 0 then
                do
                    if np = 'N' then
                        raise syntax 93.900 array('Single Quotes Must Be Matched In The Negative Pattern')
                    else
                        raise syntax 93.900 array('Single Quotes Must Be Matched In The Positive Pattern')
                end
        end

-- be sure double quotes are paired
    if the_mask~pos('"') > 0 then
        do
            if the_mask~countStr('"') // 2 \= 0 then
                do
                    if np = 'N' then
                        raise syntax 93.900 array('Double Quotes Must Be Matched In The Negative Pattern')
                    else
                        raise syntax 93.900 array('Double Quotes Must Be Matched In The Positive Pattern')
                end
        end

-- examine the first character, if it is a quote, there must be a prefix
    first_char = the_mask~left(1)
    the_prefix = ''
    if first_char = '"' | first_char = "'" then
        do
            parse var the_mask (first_char)the_prefix(first_char)the_mask
        end

-- examine the last character, if it is a quote, there must be a suffix
    last_char = the_mask~right(1)
    the_suffix = ''
    if last_char = '"' | last_char = "'" then
        do
            parse var the_mask the_mask(last_char)the_suffix(last_char)
        end

-- can't be but one . in the entire pattern
    if the_mask~countStr('.') > 1 then
        do
            if np = 'N' then
                raise syntax 93.900 array('Negative Patterns Can Not Have More Than One Decimal (.)')
            else
                raise syntax 93.900 array('Positive Patterns Can Not Have More Than One Decimal (.)')
        end

-- break the pattern into the integer and decimal parts
    parse var the_mask m_int'.'m_dec

-- a # can not follow a 0 in the interger part
    fp_z = m_int~pos('0')
    if fp_z > 0 then
        do
            if m_int~pos('#',fp_z) > 0 then
                do
                    if np = 'N' then
                        raise syntax 93.900 array('A # Symbol Can Not Follow A 0 In The Integer Portion Of The Negative Pattern')
                    else
                        raise syntax 93.900 array('A # Symbol Can Not Follow A 0 In The Integer Portion Of The Positive Pattern')
                end
        end

-- a 0 can not follow a # in the decimal part
    fp_p = m_dec~pos('#')
    if fp_p > 0 then
        do
            if m_dec~pos('0',fp_p) > 0 then
                do
                    if np = 'N' then
                        raise syntax 93.900 array('A 0 Symbol Can Not Follow A # In The Decimal Portion Of The Negative Pattern')
                    else
                        raise syntax 93.900 array('A 0 Symbol Can Not Follow A # In The Decimal Portion Of The Positive Pattern')
                end
        end

-- verify that what is left consists only of the #,0. characters
    if the_mask~verify('#,0.') <> 0 then
        if np = 'N' then
            raise syntax 93.900 array('The Mask Portion Of The Negative Pattern Can Not Contain Characters Other Than "#,0."')
        else
            raise syntax 93.900 array('The Mask Portion Of The Positive Pattern Can Not Contain Characters Other Than "#,0."')

-- verify that there is no more than 1 decimal in the pattern mask
    first_d = the_mask~pos('.')
    if first_d  > 0 then
        do
            next_c = the_mask~pos(',',first_d+1)
            if next_c > 0 then
            if np = 'N' then
                raise syntax 93.900 array('A Comma Can Not Follow A Decimal In The Mask Portion Of The Negative Pattern')
            else
                raise syntax 93.900 array('A Comma Can Not Follow A Decimal In The Mask Portion Of The Positive Pattern')
        end

    select
        when np = 'P' & the_mask~pos(',') > 0 then
            pGrouping = .true
        when np = 'P' & the_mask~pos(',') < 1 then
            pGrouping = .false
        when np = 'N' & the_mask~pos(',') > 0 then
            nGrouping = .true
        when np = 'N' & the_mask~pos(',') < 1 then
            nGrouping = .false
        otherwise
            nop
    end
return 0

-- do the actual formatting of the input number
::method format
    expose pPrefix pMask pSuffix nPrefix nMask nSuffix zMask zero_suffix input have_zMask decimalSeparator groupingSeparator
    use strict arg input

-- set numeric digits to the length of input + the number of zeros in the decimal part of the patterns
    adder = 0
    parse var pMask .'.'dec_p
    ph0 = dec_p~countStr('0')
    if ph0 > adder then
        adder = ph0
    parse var nMask .'.'dec_p
    ph0 = dec_p~countStr('0')
    if ph0 > adder then
        adder = ph0
    if input~length + adder > 9 then
        numeric digits input~length + adder

    if \input~datatype('n') then
        raise syntax 93.904 array(1,arg(1))

-- perform all masking based on the absolute value of the input, but save the input for later testing
    save_input = input
    input = input~abs()

    parse var pMask pt1'.'pt2
    p2 = pt2~countStr('0')
    if p2 = 0 then
        hold = input~format(,0)
    if hold = 0 then
        save_input = 0

-- determine which mask to use
    select
        when save_input~abs = 0 & have_zMask then
            do
                output = zMask
            end
        when save_input = 0 then
            do
                output = self~doZero
            end
        when save_input > 0 then
            do
                output = self~format2(pMask)
                output = pPrefix||output||pSuffix
            end
        when save_input < 0 then
            do
                output = self~format2(nMask)
                output = nPrefix||output||nSuffix
-- the format in ~format2 may have produced a 0 value
--                if output == '-0' & \have_zMask then
--                    output = self~doZero
            end
        otherwise
            nop
    end

    if groupingSeparator = decimalSeparator then
        raise syntax 93.900 array('Grouping & Decimal Separators Can Not Be The Same')

    if groupingSeparator \= ',' then
        output = output~changeStr(',',.endOfLine)
    if decimalSeparator \= '.' then
        output = output~changeStr('.',decimalSeparator)
    output = output~changeStr(.endOfLine,groupingSeparator)
return output

-- internal use only
::method format2
    expose output input zMask have_zMask groupingSize
    use strict arg mask2use

-- set numeric digits to the length of input + the number of zeros in the decimal part of the patterns
    adder = 0
    parse var pMask .'.'dec_p
    ph0 = dec_p~countStr('0')
    if ph0 > adder then
        adder = ph0
    parse var nMask .'.'dec_p
    ph0 = dec_p~countStr('0')
    if ph0 > adder then
        adder = ph0
    if input~length + adder > 9 then
        numeric digits input~length + adder

    parse var mask2use m_part1'.'m_part2
-- format input based on the length of the decimal portion of the mask
    if m_part2 \== '' then
        do
            input = input~format(,m_part2~length)
        end
    else
        input = input~format(,0)

    select
        when input = 0 & \have_zMask then
            output = 0
        when input = 0 then
            output = zMask
        otherwise
            do
-- strip any trailing 0 or . from the formated result is no decimal places specified
                if m_part2 \== '' & m_part_2~pos('0') < 1 then
                    do
                        input = input~strip('t','0')
                        input = input~strip('t','.')
                    end

                parse var input pt1'.'pt2
-- deal with pt1 - integer portion
                if m_part1~pos(',') > 0 then
                    do
-- we need grouping
                        i_int = pt1~reverse
                        output = ''
                        do while i_int <> ''
                            parse var i_int thousand_part =(groupingSize+1) i_int
                            if output == '' then
                                output = thousand_part
                            else
                                output = output','thousand_part
                        end
                        output = output~reverse
                    end
                else
-- no grouping needed
                    output = pt1

-- pad with any 0 place holders - save our results in op_pt1
                width = m_part1~length
                num01 = m_part1~countStr('0')
                if num01 > 0 then
                    do
                        if output~length < num01 then
                            do until output~length = num01
                                output = '0'||output
                            end
                    end

                if output = '' then
                    output = 0

                if num0 < 1 then
                    output = output~strip('l','0')

                op_pt1 = output

-- deal with pt2 - decimal portion
                if pt2 = '' then
                    do
-- no decimal characters after format, so place 0 place holders
                        num02 = m_part2~countStr('0')
                        if num02 < 1 then
                            output = op_pt1
                        else
                            do
                                output = op_pt1'.'||'0'~copies(num02)
                            end
                    end
                else
                    do
-- pad decimal characters with 0 place holders if any and place the results in op_pt2
                        op_pt2 = pt2
                        width = m_part2~length
                        num02 = m_part2~countStr('0')
                        if num02 > 1 then
                            do
                                if op_pt2~length < width then
                                    do until op_pt2~length = width
                                        op_pt2 = op_pt2||'0'
                                    end
                            end
-- put the pieces together
                        output = op_pt1'.'op_pt2
                    end

                if num01 = 0 then
                    do
                        output = output~strip('l','0')
-- format may have produced a 0 value
                        if output = '' then
                            output = 0
                    end
            end
    end
return output

::method doZero
    expose pPrefix pMask pSuffix
    parse var pMask pt1'.'pt2
    p0 = pt1~countStr('0')
    p1 = pt2~countStr('0')
    output = pPrefix||'0'~copies(p0)
    if p1 > 0 then
        output = output||'.'||'0'~copies(p1)
    output = output||pSuffix
    if output = '' then
        output = 0
return output

