math.js.flow 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. // @flow
  2. import defaultSymbolMap from './presets/defaultSymbols'
  3. import PolishedError from '../internalHelpers/_errors'
  4. const unitRegExp = /((?!\w)a|na|hc|mc|dg|me[r]?|xe|ni(?![a-zA-Z])|mm|cp|tp|xp|q(?!s)|hv|xamv|nimv|wv|sm|s(?!\D|$)|ged|darg?|nrut)/g
  5. // Merges additional math functionality into the defaults.
  6. function mergeSymbolMaps(additionalSymbols?: Object): Object {
  7. const symbolMap = {}
  8. symbolMap.symbols = additionalSymbols
  9. ? { ...defaultSymbolMap.symbols, ...additionalSymbols.symbols }
  10. : { ...defaultSymbolMap.symbols }
  11. return symbolMap
  12. }
  13. function exec(operators: Array<any>, values: Array<number>): Array<number | string> {
  14. const op = operators.pop()
  15. values.push(op.f(...[].concat(...values.splice(-op.argCount))))
  16. return op.precedence
  17. }
  18. function calculate(expression: string, additionalSymbols?: Object): number {
  19. const symbolMap = mergeSymbolMaps(additionalSymbols)
  20. let match
  21. const operators = [symbolMap.symbols['('].prefix]
  22. const values = []
  23. const pattern = new RegExp( // Pattern for numbers
  24. `\\d+(?:\\.\\d+)?|${
  25. // ...and patterns for individual operators/function names
  26. Object.keys(symbolMap.symbols)
  27. .map(key => symbolMap.symbols[key])
  28. // longer symbols should be listed first
  29. // $FlowFixMe
  30. .sort((a, b) => b.symbol.length - a.symbol.length)
  31. // $FlowFixMe
  32. .map(val => val.regSymbol)
  33. .join('|')
  34. }|(\\S)`,
  35. 'g',
  36. )
  37. pattern.lastIndex = 0 // Reset regular expression object
  38. let afterValue = false
  39. do {
  40. match = pattern.exec(expression)
  41. const [token, bad] = match || [')', undefined]
  42. const notNumber = symbolMap.symbols[token]
  43. const notNewValue = notNumber && !notNumber.prefix && !notNumber.func
  44. const notAfterValue = !notNumber || (!notNumber.postfix && !notNumber.infix)
  45. // Check for syntax errors:
  46. if (bad || (afterValue ? notAfterValue : notNewValue)) {
  47. throw new PolishedError(37, match ? match.index : expression.length, expression)
  48. }
  49. if (afterValue) {
  50. // We either have an infix or postfix operator (they should be mutually exclusive)
  51. const curr = notNumber.postfix || notNumber.infix
  52. do {
  53. const prev = operators[operators.length - 1]
  54. if ((curr.precedence - prev.precedence || prev.rightToLeft) > 0) break
  55. // Apply previous operator, since it has precedence over current one
  56. } while (exec(operators, values)) // Exit loop after executing an opening parenthesis or function
  57. afterValue = curr.notation === 'postfix'
  58. if (curr.symbol !== ')') {
  59. operators.push(curr)
  60. // Postfix always has precedence over any operator that follows after it
  61. if (afterValue) exec(operators, values)
  62. }
  63. } else if (notNumber) {
  64. // prefix operator or function
  65. operators.push(notNumber.prefix || notNumber.func)
  66. if (notNumber.func) {
  67. // Require an opening parenthesis
  68. match = pattern.exec(expression)
  69. if (!match || match[0] !== '(') {
  70. throw new PolishedError(38, match ? match.index : expression.length, expression)
  71. }
  72. }
  73. } else {
  74. // number
  75. values.push(+token)
  76. afterValue = true
  77. }
  78. } while (match && operators.length)
  79. if (operators.length) {
  80. throw new PolishedError(39, match ? match.index : expression.length, expression)
  81. } else if (match) {
  82. throw new PolishedError(40, match ? match.index : expression.length, expression)
  83. } else {
  84. return values.pop()
  85. }
  86. }
  87. function reverseString(str: string): string {
  88. return str.split('').reverse().join('')
  89. }
  90. /**
  91. * Helper for doing math with CSS Units. Accepts a formula as a string. All values in the formula must have the same unit (or be unitless). Supports complex formulas utliziing addition, subtraction, multiplication, division, square root, powers, factorial, min, max, as well as parentheses for order of operation.
  92. *
  93. *In cases where you need to do calculations with mixed units where one unit is a [relative length unit](https://developer.mozilla.org/en-US/docs/Web/CSS/length#Relative_length_units), you will want to use [CSS Calc](https://developer.mozilla.org/en-US/docs/Web/CSS/calc).
  94. *
  95. * *warning* While we've done everything possible to ensure math safely evalutes formulas expressed as strings, you should always use extreme caution when passing `math` user provided values.
  96. * @example
  97. * // Styles as object usage
  98. * const styles = {
  99. * fontSize: math('12rem + 8rem'),
  100. * fontSize: math('(12px + 2px) * 3'),
  101. * fontSize: math('3px^2 + sqrt(4)'),
  102. * }
  103. *
  104. * // styled-components usage
  105. * const div = styled.div`
  106. * fontSize: ${math('12rem + 8rem')};
  107. * fontSize: ${math('(12px + 2px) * 3')};
  108. * fontSize: ${math('3px^2 + sqrt(4)')};
  109. * `
  110. *
  111. * // CSS as JS Output
  112. *
  113. * div: {
  114. * fontSize: '20rem',
  115. * fontSize: '42px',
  116. * fontSize: '11px',
  117. * }
  118. */
  119. export default function math(formula: string, additionalSymbols?: Object): string {
  120. const reversedFormula = reverseString(formula)
  121. const formulaMatch = reversedFormula.match(unitRegExp)
  122. // Check that all units are the same
  123. if (formulaMatch && !formulaMatch.every(unit => unit === formulaMatch[0])) {
  124. throw new PolishedError(41)
  125. }
  126. const cleanFormula = reverseString(reversedFormula.replace(unitRegExp, ''))
  127. return `${calculate(cleanFormula, additionalSymbols)}${
  128. formulaMatch ? reverseString(formulaMatch[0]) : ''
  129. }`
  130. }