home *** CD-ROM | disk | FTP | other *** search
/ Programming Languages Suite / ProgLangD.iso / VCAFE.3.0A / Main.bin / CurrencyEngine.java < prev    next >
Text File  |  1998-12-09  |  14KB  |  365 lines

  1. /*
  2.   Engine class for support of currency field input.
  3.  
  4.   Created 5/4/98 by Paul Lancaster.
  5. */
  6.  
  7. package com.symantec.itools.swing;
  8.  
  9. import java.math.BigDecimal;   // used for rounding
  10. import java.text.NumberFormat;
  11. import java.text.DecimalFormat;
  12. import java.text.DecimalFormatSymbols;
  13.  
  14. public class CurrencyEngine {
  15.   // default constructor
  16.   public CurrencyEngine() {
  17.     this (  getDefaultDecimalSeparator() , 
  18.             getDefaultGroupingSeparator() );
  19.   }  
  20.   
  21.   public CurrencyEngine( char decimalPoint , char separator ) {
  22.       
  23.       super();
  24.       setDecimalPoint ( decimalPoint );
  25.       setSeparator ( separator );
  26.       
  27.   }  
  28.  
  29.   public void    setCommas            (boolean b) { _commas              = b    ; }
  30.   public void    setCurrencyLeading   (boolean b) { _currencySymbolLeads = b    ; }
  31.   public void    setATMmode           (boolean b) { _ATMmode             = b    ; }
  32.   public void    setCurrencySymbol    (String  s) { _currencySymbol      = s    ; }
  33.   public void    setDecimalPoint      (char    c) { _decimalPoint        = c    ; }
  34.   public void    setSeparator         (char    c) { _separator           = c    ; }
  35.   public void    setDigitsAfterDecimal(int     c) { _digitsAfterDecimal  = c    ; }
  36.   public boolean getCommas            (         ) { return _commas              ; }
  37.   public boolean getCurrencyLeading   (         ) { return _currencySymbolLeads ; }
  38.   public boolean getATMmode           (         ) { return _ATMmode             ; }
  39.   public String  getCurrencySymbol    (         ) { return _currencySymbol      ; }
  40.   public char    getDecimalPoint      (         ) { return _decimalPoint        ; }
  41.   public char    getSeparator         (         ) { return _separator           ; }
  42.   public int     getDigitsAfterDecimal(         ) { return _digitsAfterDecimal  ; }
  43.  
  44.   /*  Called to initialize the display of the masked data.
  45.       The first parameter is the current data in the field.
  46.       The second parameter holds the string that should be displayed.
  47.       The return value is the initial caret position.
  48.   */
  49.   public int initDisplay(String data, StringBuffer newData) {
  50.     if (data.length() == 0) {   // no input data
  51.       normalize(newData);
  52.     } else {  // data coming in
  53.       newData.append(data);
  54.       scale(newData);
  55.     }
  56.     return _ATMmode ? newData.length() : 0;
  57.   }
  58.  
  59.   /* This is the main workhorse method.
  60.      It's called for every key stroke corresponding to displayable
  61.      characters once editing begins.
  62.      The 1st parameter is the user keystroke event object.
  63.      The 2nd parameter is the current cursor position (zero based).
  64.      The 3rd parameter is the current text from the component.
  65.      The 4th parameter is output and is what should be displayed in the component.
  66.      The 5th and 6th parameters are the selection start and end, respectively.
  67.      The return value is the new cursor position within the "newData"
  68.      parameter (zero based), unless it is negative, in which case:
  69.      -1 means the input is inconsistent.
  70.   */
  71.   public int processKey(java.awt.event.KeyEvent e, int pos, String data,
  72.                         StringBuffer newData, int selStart, int selEnd) {
  73.     char key = e.getKeyChar();
  74.     newData.append(data);    // init output to input
  75.     int decpos = data.indexOf(_decimalPoint);
  76.     int datalen = data.length();
  77.     int keyCode = e.getKeyCode();
  78.     if (_ATMmode) {
  79.       switch (keyCode) {
  80.       case e.VK_LEFT:
  81.         return pos - (pos == 0 ? pos : (pos == decpos + 1 ? 2 : 1));
  82.       case e.VK_RIGHT:
  83.         return pos + (pos == datalen ? 0 : (pos == decpos - 1 ? 2 : 1));
  84.       case e.VK_BACK_SPACE:
  85.       case e.VK_DELETE:
  86.         if (pos == decpos)  // don't allow decimal point deletion
  87.           return -1;
  88.         if (selStart < selEnd) {
  89.           clearSelectedText(selStart, selEnd, newData, true);
  90.           return selStart;
  91.         }
  92.         deleteChar(newData, pos == datalen ? pos - 1 : pos);
  93.         normalize(newData);
  94.         int delta = newData.length() - datalen;
  95.         return pos == decpos - 1 ? (decpos + (0 == delta ? 1 : 0)) : pos + delta;
  96.       }  // end switch on navigation key
  97.     } else {  // not ATM mode
  98.       // Have to handle backspace & delete in non-ATM mode to ensure
  99.       // right justification occurs if component is awt.TextField
  100.       switch (keyCode) {
  101.       case e.VK_BACK_SPACE:
  102.         if (selEnd == selStart && pos > 0) {
  103.           clearSelectedText(pos - 1, pos, newData, false);
  104.           return pos - 1;
  105.         } else {
  106.           clearSelectedText(selStart, selEnd, newData, false);
  107.           return pos;
  108.         }
  109.       case e.VK_DELETE:
  110.         if (selEnd == selStart && pos < datalen)
  111.           clearSelectedText(pos, pos + 1, newData, false);
  112.         else
  113.           clearSelectedText(selStart, selEnd, newData, false);
  114.         return pos;
  115.       }  // end switch on navigation key
  116.     }
  117.     if (!Character.isDigit(key)) {  // input not a digit?
  118.       if (key != _decimalPoint || _ATMmode)
  119.         return -1;    // must be decimal point if not digit
  120.       if (decpos != -1)
  121.         return pos <= decpos ? decpos + 1 : -1;
  122.     }
  123.     clearSelectedText(selStart, selEnd, newData, true);
  124.     if (_ATMmode) {
  125.       //if (selStart < selEnd && decpos < selStart) {
  126.       if (selStart < selEnd ) {
  127.         //this is a quick fix to avoid StringIndexOutOfBoundsExceptions
  128.         //@todo:handle cases (properly) when a digit to the left of decimal
  129.         //      separator shifts to the right when selection is deleted
  130.         newData.insert(selStart, key);
  131.       }else{
  132.         newData.insert(pos, key);
  133.       }
  134.       normalize(newData);
  135.       return selStart < selEnd ? selStart : pos + newData.length() - datalen;
  136.     } else{
  137.       //newData.insert(pos, key);
  138.       //this is a quick fix to avoid StringIndexOutOfBoundsExceptions
  139.       if (selStart < selEnd ) {
  140.         pos = selStart;
  141.         newData.insert(selStart, key);
  142.       }else{
  143.         newData.insert(pos, key);
  144.       }
  145.     }
  146.     return pos + 1;
  147.   }
  148.  
  149.   // Return true iff the engine handles the given key stroke.
  150.   public boolean isHandledKey(java.awt.event.KeyEvent e) {
  151.     if (!Character.isISOControl(e.getKeyChar()))
  152.       return true;  // we handle all non-control characters
  153.     switch (e.getKeyCode()) {  // here are the controls we handle
  154.     case e.VK_BACK_SPACE:
  155.     case e.VK_DELETE:
  156.       return true;
  157.     case e.VK_RIGHT:
  158.     case e.VK_LEFT:
  159.       return _ATMmode && !e.isShiftDown();
  160.     }
  161.     return false;  // don't handle control chars by default
  162.   }
  163.  
  164.   public void postFormat(String data, StringBuffer newData) {
  165.     newData.append(data);
  166.     scale(newData);
  167.     data = newData.toString();
  168.     
  169.     newData.setLength(0);
  170.     if ( _currencySymbolLeads ) {
  171.         newData.append(_currencySymbol);
  172.     }
  173.     
  174.     int j = newData.length();
  175.     newData.append(data);
  176.     
  177.     if (!_currencySymbolLeads) { 
  178.         newData.append(_currencySymbol);
  179.     }
  180.     int decpos = data.indexOf(_decimalPoint);
  181.     
  182.     // if number of digits after decimal == 0, 
  183.     // scale() returns a number-string with no decimal point
  184.     // In such cases, it is virtually at the end.
  185.     if ( decpos == -1 ) {
  186.         decpos = data.length();
  187.     }
  188.     
  189.     for (int i = 0; i < decpos; i++, j++)
  190.       //if (_commas && i > 0 && i % 3 == decpos % 3)
  191.         if (_commas && i > 0 && i % defDecimalFormat.getGroupingSize() == decpos % defDecimalFormat.getGroupingSize())
  192.         newData.insert(j++, _separator);
  193.     
  194.     if ( debug ) {
  195.         System.out.println ( "postformat : " + newData );
  196.     }
  197.     
  198.   }
  199.  
  200.   public void cut(StringBuffer newData, int selStart, int selEnd) {
  201.     clearSelectedText(selStart, selEnd, newData, true);
  202.   }
  203.  
  204.   public void paste(StringBuffer data, String pasteData, int pos, int selStart, int selEnd) {
  205.     clearSelectedText(selStart, selEnd, data, false);
  206.     String s = data.toString();
  207.     int decpos = s.indexOf(_decimalPoint);
  208.     data.setLength(0);
  209.  
  210.     // Sanitize the paste data to remove non-digits.
  211.     StringBuffer pd = new StringBuffer();
  212.     int pastelen = pasteData.length();
  213.     for (int i = 0; i < pastelen; i++) {
  214.       char c = pasteData.charAt(i);
  215.       if (Character.isDigit(c) || (decpos == -1 && _decimalPoint == c))
  216.         pd.append(c);
  217.     }
  218.  
  219.     data.append(s.substring(0, pos) + pd.toString() + s.substring(pos, s.length()));
  220.     if (_ATMmode)
  221.       normalize(data);
  222.   }
  223.  
  224.   void clearSelectedText(int selStart, int selEnd, StringBuffer data, boolean norm) {
  225.     int selLen = selEnd - selStart;
  226.     if (selLen > 0) {            // only if selected text exists
  227.       String s = data.toString();
  228.       int oldlen = data.length();
  229.       data.insert(0, s.substring(0, selStart) + s.substring(selEnd));
  230.       data.setLength(oldlen - selLen);
  231.       if (_ATMmode && norm)
  232.         normalize(data);
  233.     }
  234.   }
  235.  
  236.   /* The input is a digit string (possibly with a decimal point) that's presumed
  237.      to be the mantissa of a floating point value whose exponent is the number of digits
  238.      minus the digitsAfterDecimal property.  The incoming decimal point position is
  239.      ignored unless it's found to be correct. It's adjusted to be to the left of
  240.      exactly digitsAfterDecimal digits, with zero fill as required.  Values less
  241.      than unity are given a leading zero.
  242.  
  243.       Examples: (with _digitsAfterDecimal == 2)
  244.  
  245.         Input     Output
  246.          25         0.25
  247.         1.375      13.75
  248.         295.4      29.54
  249.   */
  250.   private void normalize(StringBuffer sigfigs) {
  251.     int decpos = sigfigs.toString().indexOf(_decimalPoint);
  252.     int lod = sigfigs.toString().length() - 1 - _digitsAfterDecimal;  // # digits left of dec. pt.
  253.     if (decpos != lod || decpos < 1) { // if decimal point's absent or in wrong place
  254.       if (decpos != -1) {              // remove existing decimal point
  255.         deleteChar(sigfigs, decpos);
  256.         lod--;              // since first LOD calculation assumed no dec. pt.
  257.       }
  258.       while (lod++ < 0)  // zero fill to right of decimal if needed
  259.         sigfigs.insert(0, '0');
  260.       sigfigs.insert(sigfigs.length() - _digitsAfterDecimal, _decimalPoint);
  261.     }
  262.  
  263.     // Trim down to at most one leadng zero
  264.     while (sigfigs.charAt(0) == '0' && sigfigs.charAt(1) != _decimalPoint)
  265.       deleteChar(sigfigs, 0);
  266.   }
  267.  
  268.   /*  Reformats the parameter to have the right number of digits after the
  269.       decimal point.  The position (or absence) of the decimal point on entry
  270.       determines the exponent.  If the input number of digits to the right
  271.       of the decimal is less than _digitsAfterDecimal, zeros are appended.
  272.       If there are more than _digitsAfterDecimal digits after the decimal,
  273.       rounding is done.
  274.  
  275.       Examples: (with _digitsAfterDecimal == 2)
  276.  
  277.           Input      Output
  278.            25         25.00
  279.           1.375        1.38
  280.           295.4      295.40
  281.          999.995    1000.00
  282.   */
  283.   private void scale(StringBuffer newData) {
  284.     if (newData.length() == 0) {
  285.         newData.append('0');
  286.     }
  287.     
  288.     String data = newData.toString();
  289.     double dblData = 0;
  290.     
  291.     //hopefully nobody would use this as their decimal separator
  292.     char tempChar = '~';
  293.     if ( _separator != getDefaultGroupingSeparator() ) {
  294.         //first replace it with some other character
  295.         data = data.replace ( _separator , tempChar );
  296.     }
  297.     if ( _decimalPoint != getDefaultDecimalSeparator() ) {
  298.         // DecimalFormat.parse needs locale specific decimal point 
  299.         data = data.replace ( _decimalPoint , getDefaultDecimalSeparator() );
  300.     }
  301.     if ( _separator != getDefaultGroupingSeparator() ) {
  302.         data = data.replace ( tempChar , getDefaultGroupingSeparator() );
  303.     }
  304.     //parse using the locale specific decimal format
  305.     try{
  306.         dblData = defDecimalFormat.parse( data ).doubleValue();
  307.     }catch (Exception e ){
  308.         // in case of exceptions throw a number format exception 
  309.         // which is a runtime exception as aginst parse exception
  310.         // which is a language exception
  311.         throw new NumberFormatException ( e.getMessage() );
  312.     }
  313.     data = (new BigDecimal(dblData)).setScale(_digitsAfterDecimal, BigDecimal.ROUND_HALF_UP).toString();
  314.     // data would now have '.' as decimal separator
  315.     // Restore things to what they were earlier
  316.     if (_decimalPoint != '.' ) {
  317.         data = data.replace( '.' , _decimalPoint );
  318.     }
  319.     if ( debug ) {
  320.         System.out.println ( "scale : " + data );
  321.     }
  322.     
  323.     newData.setLength(0);
  324.     newData.append(data);
  325.   }
  326.  
  327.   // Delete the character at the given position from the given StringBuffer
  328.   void deleteChar(StringBuffer data, int pos) {
  329.     int len = data.length();
  330.     if (pos >= 0 && pos < len) {
  331.       String s = data.toString();
  332.       data.insert(0, s.substring(0, pos) + (pos < len -1 ? s.substring(pos + 1) : ""));
  333.       data.setLength(len - 1);
  334.     }
  335.   }
  336.   
  337.   // Other utility methods
  338.   
  339.     /**
  340.      * Returns the locale specific default decimal separator.
  341.      */
  342.     public static char getDefaultDecimalSeparator () {
  343.         return defDecimalFormat.getDecimalFormatSymbols().getDecimalSeparator();
  344.     }
  345.     
  346.     /**
  347.      * Returns the locale specific default grouping separator.
  348.      */
  349.     public static char getDefaultGroupingSeparator () {
  350.         return defDecimalFormat.getDecimalFormatSymbols().getGroupingSeparator();
  351.     }
  352.  
  353.   // Variables
  354.   boolean _commas              = true ;  // true to display thousands separator
  355.   boolean _currencySymbolLeads = true ;  // true if currency symbol precedes value
  356.   boolean _ATMmode             = false;  // true for right-to-left data entry
  357.   String  _currencySymbol      = "$"  ;
  358.   char    _decimalPoint        = getDefaultDecimalSeparator() ;
  359.   char    _separator           = getDefaultGroupingSeparator() ;
  360.   int     _digitsAfterDecimal  = 2    ;
  361.   
  362.     private static DecimalFormat defDecimalFormat = (DecimalFormat) NumberFormat.getNumberInstance();
  363.     private static boolean debug = false ;        
  364. }
  365.