Applescript之谜:一个科学记数法转换子程序

Applescript mystery: A scientific notation conversion subroutine

我手上有点神秘。我找到了一个子程序 (http://macscripter.net/viewtopic.php?id=27520) 将一个科学数字转换成一串数字。但是,无论我如何尝试,它似乎都会删除剩余的数字。

“1.23456789E+4”应该变成“12345.6789”。相反,它只是 returns“12345”。

试试 运行 下面的代码,您就会明白我的意思了。我调用一个对话框来公开结果:

set xx to 1.23456789E+4
set yy to number_to_string(xx)
display dialog yy

on number_to_string(this_number)
    set this_number to this_number as string
    set deci to character 2 of (0.5 as text)
    set x to the offset of deci in this_number
    set z to the offset of "E" in this_number
    if this_number contains "E+" then
        set y to the offset of "+" in this_number
        set the decimal_adjust to characters (y - (length of this_number)) thru ¬
            -1 of this_number as string as number
        if x is not 0 then
            set the first_part to characters 1 thru (x - 1) of this_number as string
        else
            set the first_part to ""
        end if
        set the second_part to characters (x + 1) thru (z - 1) of this_number as string
        set the converted_number to the first_part
        repeat with i from 1 to the decimal_adjust
            try
                set the converted_number to ¬
                    the converted_number & character i of the second_part
            on error
                set the converted_number to the converted_number & "0"
            end try
        end repeat
        return the converted_number
    else
        if this_number contains "E-" then
            set y to the offset of "-" in this_number
            if x is not 0 then
                set the first_part to text 1 thru (x - 1) of this_number
            else
                set the first_part to ""
            end if
            set the second_part to text (x + 1) thru (z - 1) of this_number
            set the converted_number to the first_part & second_part
            set n to text (y + 1) thru -1 of this_number as number
            set zero to "0."

            if n > 1 then
                repeat (n - 1) times
                    set zero to zero & "0"
                end repeat
            end if
            set converted_number to zero & converted_number
        else
            set converted_number to this_number
        end if
    end if

    return converted_number
end number_to_string

至于为什么你的 AppleScript 代码不起作用

你的代码只关注指数,而不关注尾数[的位数=141=](指数前的小数部分)。

因此,以1.23456789E+4为输入,严格提取尾数的4位组成结果,不管尾数有多少位:12345678 前 4 位数字 ,得到 12345.


在 AppleScript 中做到这一点很重要,所以 我建议使用 do shell script 和 shell 命令,使用 bc, a POSIX arbitrary-precision calculation utility,这将得到你快多了:

set xx to "1.23456789E+4" # define as *string* to avoid rounding errors

# Perform transformation via handler defined below.
set yy to my toDecFraction(xx)

display alert yy

on toDecFraction(numStr)

    local maxDecPlaces

    # For *negative* exponents: set the maximum number of decimal places in the result.
    # For *positive* exponents: the number of decimal places in the result is 
    # automatically chosen to accommodate all digits.
    # In either case: the max. number of decimal places supported is 2,147,483,647.
    set maxDecPlaces to 32

    do shell script "{ printf 'scale=" & maxDecPlaces & ¬
        "; '; sed -E 's/[eE]\+?/*10^/g' <<<" & quoted form of (numStr as text) & "; } | 
          bc | tr -d '\n\' | 
            sed -E -e '/\./!b' -e 's/\.0+$//;t' -e 's/0+$//; s/^(-?)(\.)/\10\2/'"

end toDecFraction
  • printf 'scale=<n>;',当通过管道传输到 bc 时,指示它在 negative 的情况下使用 <n> 小数位的精度指数;如果指数是 positivebc 会自动选择保留所有数字的精度。
    • 小数位数的上限是假设的 2,147,483,647(!) (2^32/2-1),但请注意,您为 maxDecPlaces 选择的数字越高(在在负指数的情况下)或输入的小数位数越多(在正指数的情况下),转换所需的时间越长,尽管 实际上,在性能上几乎没有差异,比如, 32 对 200(!) 个小数位。请注意,如果限制太低,则会发生截断,而不是舍入。
    • 可以计算精确保留所有数字所需的小数位数,但它需要非平凡的词法分析, 所以选择一个足够高的上限是一种务实的妥协。
  • sed -E 's/[eE]\+?/*10^/g'' 将科学计数法重新格式化为 bc 可以求值的完全等价的算术表达式;例如。:
    • 1e2 -> 1*10^2
    • .3e+1 -> .3*10^1
    • 2.5e-2 -> 2.5*10^-2
  • 将该表达式传递给 bc 只是将其结果 打印为小数部分 ,小数位数与输入所暗示的一样多(在正指数的情况下) ,或通过变量指定 scale (在负指数的情况下)
  • tr -d '\n\' 需要删除 \ 个字符。以及 bc 在输出长度超过 70 个字符的数字时插入的换行符。
  • sed -E -e '/\./!b' -e 's/\.0+$//;t' -e 's/0+$//; s/^(-?)(\.)/\10\2/' 清理结果从结果中删除尾随零(如果没有保留小数位,还删除小数点),前置 0,如果(的绝对值)结果是 < 1.

:

  • 如果结果的整数部分是0,它打印,所以,例如,1e-2 打印为 0.01,这在 AppleScript 中很正常 - 而不是 .01
    • 如果您不想要前导 0,请将上面代码中的 -e 's/0+$//; s/^(-?)(\.)/\10\2/' 替换为 -e 's/0+$//'
  • bc 是设计使然 不是 语言环境感知 ,因此 基数字符 ("decimal point") 它期望输入并产生输出是 always .

为了比较,这里有一个使用 bash 代码执行转换的处理程序 词法 - 你可以看,滚动自己的转换的努力是不平凡的 - 并且在 AppleScript 中会更加冗长。

实际上,这两种方法的表现大致相同。 此解决方案的优点是小数位数没有限制,所有数字都会自动保留并且无法识别的数字字符串会可靠地引发错误.

set xx to "1.23456789E+4" # define as *string* to avoid rounding errors

# Perform transformation via handler defined below.
set yy to my toDecFraction(xx)

display alert yy

# SYNOPSIS
#   toDecFraction(numString)
# DESCRIPTION
#   Textually reformats the specified number string from decimal exponential (scientific) notation
#   (e.g., 1.234e+2) to a decimal fraction (e.g., 123.4).
#   Leading and trailing whitespace is acceptable.
#   Input that is in integer form or already a decimal fraction is accepted, and echoed *unmodified*.
#   No fractional part is output if there is none; e.g., '1.2e1' results in '12'.
#   Numbers with an integer part of 0 are output with the leading zero (e.g. '0.1', not '.1')
#   Unrecognized number strings result in an error.
#   There is no limit on the number of decimal places and there are no rounding errors, given that
#   the transformation is purely *lexical*.
#   NOTE: This function is NOT locale-aware: a '.' must always be used as the radix character.
# EXAMPLES
#   my toDecFraction('1.234567e+2') # -> '123.4567'
#   my toDecFraction(toDecFraction '+1e-3') # -> '0.001'
#   my toDecFraction('-1.23e+3') # -> '-1230'
#   my toDecFraction ('1e-1') # -> '0.01'
on toDecFraction(numStr)
    try
        do shell script "
toDecFraction() {
  local numStr leadingZero sign intPart fractPart expSign exponent allDigits intDigitCount intDigits fractDigits padCount result
  { [[  == '--' ]] && shift; } || { [[  == '-z' ]] && { leadingZero=1; shift; } }
  read -r numStr <<<\"\" # trim leading and trailing whitespace
  # Parse into constituent parts and fail, if not recognized as decimal integer / exponential notation.
  [[ $numStr =~ ^([+-]?)([[:digit:]]+)?\.?(([[:digit:]]+)?([eE]([+-]?)([[:digit:]]+))?)?$ ]] || return 1
  sign=${BASH_REMATCH[1]} intPart=${BASH_REMATCH[2]}
  fractPart=${BASH_REMATCH[4]} expSign=${BASH_REMATCH[6]} exponent=${BASH_REMATCH[7]}
  # If there's neither an integer nor a fractional part, fail.
  [[ -n $intPart || -n $fractPart ]] || return 1
  # debugging: echo \"[$sign][$intPart].[$fractPart]e[$expSign][$exponent]\"
  # If there's no exponent involved, output the number as is 
  # (It is either an integer or already a decimal fraction.)
  [[ -n $exponent ]] || { echo \"\"; return 0; }
  allDigits=${intPart}${fractPart}
  # Calculate the number of integer digits in the resulting decimal fraction,
  # after resolving the exponent.
  intDigitCount=$(( ${#intPart} + ${expSign}${exponent} ))
  # If the sign was an explicit +, set it to the empty string - we don't want to output it.
  [[ $sign == '+' ]] && sign=''
  if (( intDigitCount > 0 )); then # at least 1 integer digit
    intDigits=${allDigits:0:intDigitCount}
    padCount=$(( intDigitCount - ${#intDigits} ))
    (( padCount > 0 )) && intDigits=${intDigits}$(printf \"%${padCount}s\" | tr ' ' '0')
    fractDigits=${allDigits:intDigitCount} # determine what goes after the radix character
    result=${sign}${intDigits}${fractDigits:+.}${fractDigits}
    # Remove leading zeros, if any.
    [[ $result =~ ^0+([^0].*)?$ ]] && result=\"${BASH_REMATCH[1]}\"
  else # result is < 1
    padCount=$(( -intDigitCount ))
    result=${sign}${leadingZero:+0}.$(printf \"%${padCount}s\" | tr ' ' '0')${intPart}${fractPart}
  fi
  # Trim an empty fractional part, and ensure that if
  # the result is empty, '0' is output.
  [[ $result =~ ^([^.]*)\.0+$ ]] && result=\"${BASH_REMATCH[1]}\"
  printf '%s\n' \"${result:-0}\"
}
toDecFraction -z " & quoted form of (numStr as text)
    on error number errNum
        error "Not recognized as a number: " & (numStr as text) number (500 + errNum)
    end try
end toDecFraction

这是 嵌入的 bash 函数,具有正确的语法突出显示:

toDecFraction() {
  local numStr leadingZero sign intPart fractPart expSign exponent allDigits intDigitCount intDigits fractDigits padCount result
  { [[  == '--' ]] && shift; } || { [[  == '-z' ]] && { leadingZero=1; shift; } }
  read -r numStr <<<"" # trim leading and trailing whitespace
  # Parse into constituent parts and fail, if not recognized as decimal integer / exponential notation.
  [[ $numStr =~ ^([+-]?)([[:digit:]]+)?\.?(([[:digit:]]+)?([eE]([+-]?)([[:digit:]]+))?)?$ ]] || return 1
  sign=${BASH_REMATCH[1]} intPart=${BASH_REMATCH[2]}
  fractPart=${BASH_REMATCH[4]} expSign=${BASH_REMATCH[6]} exponent=${BASH_REMATCH[7]}
  # If there's neither an integer nor a fractional part, fail.
  [[ -n $intPart || -n $fractPart ]] || return 1
  # debugging: echo "[$sign][$intPart].[$fractPart]e[$expSign][$exponent]"
  # If there's no exponent involved, output the number as is 
  # (It is either an integer or already a decimal fraction.)
  [[ -n $exponent ]] || { echo ""; return 0; }
  allDigits=${intPart}${fractPart}
  # Calculate the number of integer digits in the resulting decimal fraction,
  # after resolving the exponent.
  intDigitCount=$(( ${#intPart} + ${expSign}${exponent} ))
  # If the sign was an explicit +, set it to the empty string - we don't want to output it.
  [[ $sign == '+' ]] && sign=''
  if (( intDigitCount > 0 )); then # at least 1 integer digit
    intDigits=${allDigits:0:intDigitCount}
    padCount=$(( intDigitCount - ${#intDigits} ))
    (( padCount > 0 )) && intDigits=${intDigits}$(printf "%${padCount}s" | tr ' ' '0')
    fractDigits=${allDigits:intDigitCount} # determine what goes after the radix character
    result=${sign}${intDigits}${fractDigits:+.}${fractDigits}
    # Remove leading zeros, if any.
    [[ $result =~ ^0+([^0].*)?$ ]] && result="${BASH_REMATCH[1]}"
  else # result is < 1
    padCount=$(( -intDigitCount ))
    result=${sign}${leadingZero:+0}.$(printf "%${padCount}s" | tr ' ' '0')${intPart}${fractPart}
  fi
  # Trim an empty fractional part, and ensure that if
  # the result is empty, '0' is output.
  [[ $result =~ ^([^.]*)\.0+$ ]] && result="${BASH_REMATCH[1]}"
  printf '%s\n' "${result:-0}"
}

最后,这是一个更简单的shell命令,但是不推荐,因为它是受双精度浮点值固有的舍入误差影响,因此您不能保证(忠实地)保留所有数字。:

set xx to "1.23456789E+4"

set yy to do shell script "awk -v n=" & quoted form of (xx as text) & " 'BEGIN \
{ CONVFMT=\"%.11f\"; ns=\"\"(n + 0); if (ns ~ /\./) gsub(\"0+$\",\"\",ns); print ns }'"

display alert yy

该命令使用 awk 识别科学记数法的本机能力,并使用(隐式应用)printf 数字格式 "%.11f" 将结果数字转换回字符串 - 即, 11位小数;在返回结果之前,任何尾随零都被修剪(使用 gsub())。

乍一看,似乎 没问题:结果是 12345.6789但是,如果将小数位数更改为 12 (CONVFMT=\"%.12f\"),则会出现舍入错误:12345.678900000001(!)

您不会提前知道这种情况何时发生,因此如果需要忠实保留所有数字,则此方法不可行。

快速 google 搜索出现 this,但正如您所指出的,它是相同的基本代码,但不适合您。为了弥补我的愚蠢错误,我写了这个 applescript 来完成这项工作。它适用于 positive/negative 指数和 positive/negative 数字。祝你好运。

numberToString(-1.23456789E+4)

on numberToString(aNumber)
    set aNumber to aNumber as text

    -- check for a negative number
    set isNegative to false
    if character 1 of aNumber is "-" then
        set isNegative to true
        set aNumber to text 2 thru -1 of aNumber
    end if

    try
        set a to the offset of "." in aNumber
        set b to the offset of "E" in aNumber
        set c to the offset of "+" in aNumber
        set d to the offset of "-" in aNumber

        if b is 0 then -- we do not have an exponential number
            if isNegative then
                return "-" & aNumber
            else
                return aNumber
            end if
        end if

        if a is 0 then
            set firstPart to ""
        else
            set firstPart to text 1 thru (a - 1) of aNumber
        end if

        set secondPart to text (a + 1) thru (b - 1) of aNumber

        if c is 0 and d is 0 then -- assume a positive exponent
            set isPositiveExponent to true
            set thirdPart to text (b + 1) thru -1 of aNumber
        else if c is not 0 then
            set isPositiveExponent to true
            set thirdPart to text (b + 2) thru -1 of aNumber
        else
            set isPositiveExponent to false
            set thirdPart to text (b + 2) thru -1 of aNumber
        end if
        set thirdPart to thirdPart as number

        if isPositiveExponent then
            set newNumber to firstPart
            set theRemainder to secondPart
            repeat with i from 1 to thirdPart
                try
                    set newNumber to newNumber & character i of secondPart
                    if theRemainder is not "" then
                        if (count of theRemainder) is 1 then
                            set theRemainder to ""
                        else
                            set theRemainder to text 2 thru -1 of theRemainder
                        end if
                    end if
                on error
                    set newNumber to newNumber & "0"
                end try
            end repeat

            if theRemainder is not "" then
                set newNumber to newNumber & "." & theRemainder
            end if
        else


            set newNumber to ""
            set theRemainder to firstPart
            repeat with i from 1 to thirdPart
                try
                    set newNumber to character -i of firstPart & newNumber
                    if theRemainder is not "" then
                        if (count of theRemainder) is 1 then
                            set theRemainder to ""
                        else
                            set theRemainder to text 1 thru -2 of theRemainder
                        end if
                    end if
                on error
                    set newNumber to "0" & newNumber
                end try
            end repeat

            if theRemainder is not "" then
                set newNumber to theRemainder & "." & newNumber & secondPart
            else
                set newNumber to "0." & newNumber & secondPart
            end if
        end if
    on error
        if isNegative then
            return "-" & aNumber
        else
            return aNumber
        end if
    end try

    if isNegative then
        return "-" & newNumber
    else
        return newNumber
    end if
end numberToString