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

时间:2015-04-28 23:18:03

标签: macos applescript

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

“1.23456789E + 4”应变为“12345.6789”。相反,它只返回“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

2 个答案:

答案 0 :(得分:2)

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

您的代码只关注 exponent ,而不关注尾数 中的位数(数字的小数部分)在指数之前)。

因此,以1.23456789E+4作为输入,提取尾数的严格 4 数字以形成结果,而不管尾数有多少位数:1& ; 2345678前4位数,产生12345

在AppleScript中实现这一点非常重要,所以我建议使用do shell script使用bc, a POSIX arbitrary-precision calculation utility 的shell命令,这样可以更快地到达目的地:

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时,指示在否定指数的情况下,它使用<n>小数位的精度;如果指数bc会自动选择保留所有数字的精度。
    • 小数位数的上限是假设的2,147,483,647(!)(2^32/2-1),但请注意,您为maxDecPlaces选择的数字越大(如果是一个负指数)或你输入的小数位数(如果是正指数),转换所需的时间越长,但实际上在32之间的限制之间的性能差别不大。 200(!)小数位。请注意,如果限制太低,则会发生截断,而不是舍入。
    • 可以计算保留所有数字所需的精确小数位数,但它需要非常重要的词法分析,所以选择一个足够高的上层这是一种务实的妥协。
  • sed -E 's/[eE]\+?/*10^/g''将科学记数法重新格式化为bc可以评估的完全等效的算术表达式;例如。:
    • 1e2 - &gt; 1*10^2
    • .3e+1 - &gt; .3*10^1
    • 2.5e-2 - &gt; 2.5*10^-2
  • 将该表达式传递给bc只需将结果打印为小数,其输入所隐含的小数位数(如果为正指数),或者通过指定的变量scale(如果是负指数)
  • 删除tr -d '\n\'字符需要
  • \。输出超过70个字符的数字时bc插入的换行符。
  • sed -E -e '/\\./!b' -e 's/\\.0+$//;t' -e 's/0+$//; s/^(-?)(\\.)/\\10\\2/'清除结果会从结果中删除尾随零(如果没有剩下小数位,也会删除小数点),如果结果的(绝对值),则前置0是&lt; 1。

注意

  • 如果结果的整数部分为0,则 打印,因此,例如1e-2打印为{{ 1}},在AppleScript中是正常的 - 而不是0.01
    • 如果您不想要领先的.01,请使用0替换上面代码中的-e 's/0+$//; s/^(-?)(\\.)/\\10\\2/'
  • -e 's/0+$//'是设计区域设置感知,所以基数字符(&#34;小数点&#34;)是预期的输入和产出上的产品总是 bc

为了进行比较,这里有一个处理程序,它使用.代码来执行转换词法 - 正如您所看到的,滚动一个&#的努力39;自己的转变是非常重要的 - 在AppleScript中会更加冗长。

在实践中,两种方法的表现基本相同。 此解决方案的优点是,小数位数没有限制,所有数字都会自动保留无法识别的数字字符串可靠地引发错误

bash

这里是嵌入式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 { [[ $1 == '--' ]] && shift; } || { [[ $1 == '-z' ]] && { leadingZero=1; shift; } } read -r numStr <<<\"$1\" # 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 \"$1\"; 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

最后,这是一个更简单的shell命令,然而,不推荐,因为它受到双重的固有舍入误差的影响精确浮点值,所以你不能保证所有数字都被(忠实地)保存。

toDecFraction() {
  local numStr leadingZero sign intPart fractPart expSign exponent allDigits intDigitCount intDigits fractDigits padCount result
  { [[ $1 == '--' ]] && shift; } || { [[ $1 == '-z' ]] && { leadingZero=1; shift; } }
  read -r numStr <<<"$1" # 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 "$1"; 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}"
}

该命令使用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将结果数字转换回字符串 - 即小数点后11位;在返回结果之前,会修剪任何尾随零(使用"%.11f")。

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

如果发生这种情况,您事先不会知道,所以如果需要忠实保存所有数字,这种做法是不可行的。

答案 1 :(得分:-1)

快速谷歌搜索出现了this,但正如您所指出的那样,基本代码与您无关。为了弥补我的愚蠢错误,我写了这个AppleScript来完成这项工作。它适用于正/负指数和正/负数。祝你好运。

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