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 | 1× 1× 1× 1× 1× 1× 1× 1× 18× 18× 18× 18× 21× 21× 21× 21× 28× 28× 16× 37× 37× 37× 36× 36× 2× 36× 36× 36× 36× 36× 2× 36× 96× 96× 96× 96× 96× 71× 41× 30× 16× 16× 15× 32× 32× 10× 4× 32× 32× 12× 4× 65× 65× 20× 20× 65× 30× 30× 22× 65× 36× 9× 9× 9× 4× 5× 2× 3× 3× 9× 9× 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 [ @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 = @monitize(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 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 monitize: (value, formats=@getFormats()) -> if 'money' in formats value = "$#{value}" return value |