all files / src/datums/ number.cjsx

86.44% Statements 102/118
83.93% Branches 47/56
94.12% Functions 16/17
89.77% Lines 79/88
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261                                                                                                                                  18× 18× 18×   18×             21× 21× 21×         21×           28× 28×                 16×           37× 37× 37×   36×   36×       36×       36× 36× 36×   36×   36×                       96× 96× 96× 96×     96×       71× 41×   30×         16× 16× 15×       32×             32× 32× 10×       32× 32× 12×                   65×         65× 20× 20×   65× 30× 30× 22×   65×             36×         36×       36× 18×   36×                   36× 13× 36×              
 
React = require('react')
_ = require('underscore')
 
Datum = require('./datum')
 
 
ONE_BILLION = 1000000000
ONE_MILLION = 1000000
ONE_THOUSAND = 1000
 
RECOGNIZED_FORMATS = ['abbreviate','money','comma', 'percent']
 
###
  For real numbers.
 
  Only allows `/^\-?[0-9]*\.?[0-9]*$/` on input
###
module.exports = class Number extends Datum
  @displayName: "react-datum.Number"
 
  @propTypes: _.extend {}, Datum.propTypes,
    
    # format only effects display, not input.  Possible values:
    #
    # 'abbreviate' - Add M and K to numbers greater than 1 million and 1 thousand respectively
    # 'money' - display dollar sign and two decimal places zero filled
    # 'comma' - add comma separators at thousands
    # 'percent' - multiply by 100 and postfix '%'
    #
    # can be an array of formats or a single string format
    format: React.PropTypes.oneOfType [
      React.PropTypes.array
      React.PropTypes.string
    ]
    
    # rounds value to n decimal places
    decimalPlaces: React.PropTypes.number
    
    # if decimalPlaces, zeroFill={true} will round out to n places 
    zeroFill: React.PropTypes.bool
    
    # when input, validate value is at least this value on change. 
    # Can also be specified via metadata.
    minValue: React.PropTypes.number
    
    # when input, validate value is at most this value on change
    # Can also be specified via metadata.
    maxValue: React.PropTypes.number
    
  
  @defaultProps: _.extend {}, Datum.defaultProps,
    # Exceptional case: when format='money' is used without format='abbreviate',
    # this defaults to 2
    decimalPlaces: null
    
    # Exceptional case: when format='money' is used without format='abbreviate',
    # this defaults to 2, you can however change the money behavior by explicitly
    # setting zerofill to false
    zeroFill: null
    
    # This might be controversial, but our standard here at the zoo is to always use
    # thousand ticks
    format: ['comma']
    
 
  # TODO : push down this feature to Datum? with default to all   
  # will not allow characters to be entered that do not match this pattern
  charactersMustMatch: /^\-?[0-9]*\.?[0-9]*$/
 
 
  @getComaAddedValue: (value) ->
    # add thousands separater
    [wholeNumber, decimal] = value.toString().split('.')
    value = wholeNumber.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    value += '.' + decimal if decimal?
 
    return value
    
    
  ###
    fail proof conversion from sting to float that will never return NaN
  ###
  @safelyFloat: (value) ->
    Ireturn 0 unless value?
    try
      floatValue = parseFloat(value)
    catch
      console.error "unparseable float #{value}"
      return 0
      
    return Iif _.isNaN(floatValue) then 0 else floatValue
    
 
 
 
  constructor: (props) ->
    super
    @addValidations [
      # even though we don't allow key entry of non numeric patterns, Datum exposes
      # setValue() which can be called from outside to set the value of the datum
      @validateNumeric
      @validateMin
      @validateMax
    ]
 
  isAcceptableInput: (value) ->
    return value.match(@charactersMustMatch)
 
  ###
    overrides super - adds formatting
  ###
  renderValueForDisplay: ->
    modelValue = @getModelValue()
    value = parseFloat(modelValue)
    return modelValue if _.isNaN value
      
    formats = @getFormats()
 
    if 'percent' in formats
      value *= 100
      
    # we are going to convert the value to a string now.  All 
    # numberic processing should precede this comment
    value = @roundToDecimalPlaces(value, formats: formats)
 
    # at this point, value is a string with 4 decimal places zero filled
    # all formatting that works on the text value goes after here
    value = @abbreviate(value, formats)
    value = @addCommas(value, formats)
    value = @monetize(value, formats)
 
    if 'percent' in formats
      value += "%"
 
    return value
 
 
  # extends super
  renderPlaceHolder: ->
    if @getPropOrMetadata('placeholder')?
      super
    # if we don't have a placeholder, render zero by default without the placeholder classes
    return <span>0</span>
    
    
  getValueForInput: ->
    value = super
    value = value.replace(/[\s\$\,]/g, '') if value? && _.isString(value)
    Ireturn value if value in ['-', '+']
    floatVal = parseFloat(value)
    # note that we don't return the floatVal because when user is typing and gets to say, 55.
    # that would get floated to just 55 and the . would never get to the input.
    return if _.isNaN floatVal then '' else value
 
    
  getFormats: ->
    if _.isArray(@props.format) 
      return @props.format 
    else 
      return @props.format?.toString().split(' ') || []
      
      
  # extend super, ignore invalid input characters
  onChange: (event) =>
    inputValue = event.target.value
    if @isAcceptableInput(inputValue)
      super
 
 
  validateNumeric: (value) =>
    Ereturn true if @charactersMustMatch.test(value)
    if value.length > 25
      value = value.slice(0, 25) + '...'
    return "The value must be numeric. \"#{value}\" is not valid"
  
  
  validateMin: (value) =>
    minValue = @getPropOrMetadata('minValue')
    return true unless minValue?
    return true if value >= minValue
    return "The value must be greater than or equal to #{minValue}"
 
 
  validateMax: (value) =>
    maxValue = @getPropOrMetadata('maxValue')
    return true unless maxValue?
    return true if value <= maxValue
    return "The value must be less than or equal to #{maxValue}"
    
    
  ###  
    returns a string with number value input rounded to user requested props.decimalPlaces 
      and optionally zeroFilled if @props.zeroFill == true
    note that 'money', when not 'abbreviate'd should zero fill out to two decimal places 
    unless props indicate otherwise
  ###
  roundToDecimalPlaces: (value, options={}) ->
    options = _.defaults options,
      formats:  @getFormats()
      decimalPlaces: @props.decimalPlaces
      zeroFill: @props.zeroFill
    
    if 'money' in options.formats
      options.decimalPlaces ?= 2
      options.zeroFill ?= !('abbreviate' in options.formats)
      
    if options.decimalPlaces?
      value = parseFloat(value).toFixed(options.decimalPlaces)
      unless options.zeroFill
        value = parseFloat(value).toString()
        
    return value
    
  ###  
    returns a string with number value abbreviated and rounded to user 
    requested props.decimalPlaces 
  ###  
  abbreviate: (value, formats=@getFormats()) ->
    if 'abbreviate' in formats
      value = parseFloat(value)
      absValue = Math.abs(value)
      [value, affix] = if absValue >= ONE_BILLION
        [value / ONE_BILLION, "B" ]
      else if absValue >= ONE_MILLION
        [value / ONE_MILLION, "M" ]
      else Iif absValue >= ONE_THOUSAND
        [value / ONE_THOUSAND, "K"]
      else
        [value, ""]
 
      value = "#{@roundToDecimalPlaces(value, formats: formats)}"
      value += " #{affix}" if affix?.length > 0
        
    return value
        
        
  addCommas: (value, formats=@getFormats()) ->
    if 'comma' in formats
      value = Number.getComaAddedValue(value)
 
    return value
 
  ###
    If props.formats includes 'money', this method prepends the value
    displayed with '$'
    
    Override this method to do things like create an internationalized
    display of money value for another currency. 
  ###
  monetize: (value, formats=@getFormats()) ->
    if 'money' in formats
      value = "$#{value}"
    return value