How can I convert a decimal to a fraction?

2020-07-07 03:38发布

How do I convert a indefinite decimal (i.e. .333333333...) to a string fraction representation (i.e. "1/3"). I am using VBA and the following is the code I used (i get an overflow error at the line "b = a Mod b":

Function GetFraction(ByVal Num As Double) As String

    If Num = 0# Then
        GetFraction = "None"
    Else
        Dim WholeNumber As Integer
        Dim DecimalNumber As Double
        Dim Numerator As Double
        Dim Denomenator As Double
        Dim a, b, t As Double

        WholeNumber = Fix(Num)
        DecimalNumber = Num - Fix(Num)
        Numerator = DecimalNumber * 10 ^ (Len(CStr(DecimalNumber)) - 2)
        Denomenator = 10 ^ (Len(CStr(DecimalNumber)) - 2)
        If Numerator = 0 Then
            GetFraction = WholeNumber
        Else
            a = Numerator
            b = Denomenator
            t = 0

            While b <> 0
                t = b
                b = a Mod b
                a = t
            Wend
            If WholeNumber = 0 Then
                GetFraction = CStr(Numerator / a) & "/" & CStr(Denomenator / a)
            Else
                GetFraction = CStr(WholeNumber) & " " & CStr(Numerator / a) & "/" & CStr(Denomenator / a)
            End If
        End If
    End If
End Function

标签: vba
13条回答
不美不萌又怎样
2楼-- · 2020-07-07 03:59

This site seem to have a really nice implementation of this in JavaScript.

查看更多
在下西门庆
3楼-- · 2020-07-07 04:03

1/ .3333333333 = 3 because 1/3 = .3333333333333, so whatever number you get do this,

double x = 1 / yourDecimal; int y = Math.Ceil(x);

and now Display "1/" + y

查看更多
爱情/是我丢掉的垃圾
4楼-- · 2020-07-07 04:04

I would multiply by 10000000(or whatever you want depending on the precision), then simplify the resulting fraction (ie n*10000000/10000000)

查看更多
姐就是有狂的资本
5楼-- · 2020-07-07 04:10

This only works in Excel-VBA but since you had it tagged "VBA" I will suggest it. Excel has a custom "fraction" format that you can access via "Format Cells" (or ctrl-1 if you prefer). This particular number format is Excel-Specific and so does not work with the VBA.Format function. It does however work with the Excel Formula TEXT(). (Which is the Excel equivalent of VBA.Format. This can be accessed like So:

Sub Example()    
    MsgBox Excel.WorksheetFunction.Text(.3333,"# ?/?")
End Sub

To show more than one digit (Example 5/12) just up the number of question marks.

查看更多
疯言疯语
6楼-- · 2020-07-07 04:11

I know this is an old thread, but I came across this problem in Word VBA. There are so many limitations due to the 8 bit (16 digit) rounding, as well as Word VBA making decimals into scientific notation etc.. but after working around all these problems, I have a nice function I'd like to share that offers a few extra features you may find helpful.

The strategy is along the lines of what Daniel Buckner wrote. Basically: 1st) decide if it's a terminating decimal or not 2nd) If yes, just set the decimal tail / 10^n and reduce the fraction.
3rd) If it doesn't terminate, try to find a repeating pattern including cases where the repetition doesn't start right away

Before I post the function, here are a few of my observations of the risks and limitations, as well as some notes that may help you understand my approach.

Risks, limitations, explanations:

-> Optional parameter "denom" allows you to specify the denominator of the fraction, if you'd like it rounded. i.e. for inches you may want 16ths used. The fractions will still be reduced, however, so 3.746 --> 3 12/16 --> 3 3/4

-> Optional parameter "buildup" set to True will build up the fraction using the equation editor, typing the text right into the active document. If you prefer to have the function simply return a flat string representation of the fraction so you can store it programmatically etc. set this to False.

-> A decimal could terminate after a bunch of repetitions... this function would assume an infinite repetition.

-> Variable type Double trades off whole number digit for decimal digits, only allowing 16 digits total (from my observations anyway!). This function assumes that if a number is using all 16 of the available digits then it must be a repeating decimal. A large number such as 123456789876.25 would be mistaken for a repeating decimal, then returned as decimal number upon failing to find a pattern.

-> To express really large terminating decimal out of 10^n, VB can only handle 10^8 is seems. I round the origninal number to 8 decimal places, losing some accuracy perhaps.

-> For the math behind converting repeating patterns to fractions check this link

-> Use Euclidean Algorithm to reduce the fraction

Ok, here it is, written as a Word Macro:

Function as_fraction(number_, Optional denom As Integer = -1, Optional buildup As Boolean = True) As String
    'Selection.TypeText Text:="Received: " & CStr(number_) & vbCrLf
    Dim number As Double
    Dim repeat_digits As Integer, delay_digits As Integer, E_position As Integer, exponent As Integer
    Dim tail_string_test As String, tail_string_original As String, num_removed As String, tail_string_removed As String, removed As String, num As String, output As String
    output = "" 'string variable to build into the fraction answer
    number = CDbl(number_)
    'Get rid of scientific notation since this makes the string longer, fooling the function length = digits
    If InStr(CStr(number_), "E+") > 0 Then 'no gigantic numbers! Return that scientific notation junk
        output = CStr(number_)
        GoTo all_done
    End If

    E_position = InStr(CStr(number), "E") 'E- since postives were handled
    If E_position > 0 Then
        exponent = Abs(CInt(Mid(CStr(number), E_position + 1)))
        num = Mid(CStr(number_), 1, E_position) 'axe the exponent
        decimalposition = InStr(num, ".") 'note the decimal position
        For i_move = 1 To exponent
            'move the decimal over, and insert a zero if the start of the number is reached
            If InStr(num, "-") > 0 And decimalposition = 3 Then 'negative sign in front
               num = "-0." & Mid(num, InStr(num, ".") - 1, 1) & Mid(num, InStr(num, ".") + 1) 'insert a zero after the negative
            ElseIf decimalposition = 2 Then
               num = "0." & Mid(num, InStr(num, ".") - 1, 1) & Mid(num, InStr(num, ".") + 1) 'insert in front
            Else 'move the decimal over, there are digits left
               num = Mid(num, 1, decimalposition - 2) & "." & Mid(num, decimalposition - 1, 1) & Mid(num, decimalposition + 1)
               decimalposition = decimalposition - 1
            End If
        Next
    Else
        num = CStr(number_)
    End If
    'trim the digits to 15, since VB rounds the last digit which ruins the pattern. i.e. 0.5555555555555556  etc.
    If Len(num) >= 16 Then
        num = Mid(num, 1, 15)
    End If
    number = CDbl(num) 'num is a string representation of the decimal number, just to avoid cstr() everywhere
    'Selection.TypeText Text:="number = " & CStr(number) & vbCrLf

    'is it a whole number?
    If Fix(number) = number Then 'whole number
        output = CStr(number)
        GoTo all_done
    End If

    decimalposition = InStr(CStr(num), ".")
    'Selection.TypeText Text:="Attempting to find a fraction equivalent for " & num & vbCrLf
    'is it a repeating decimal? It will have 16 digits
    If denom = -1 And Len(num) >= 15 Then 'repeating decimal, unspecified denominator
        tail_string_original = Mid(num, decimalposition + 1) 'digits after the decimal
        delay_digits = -1 'the number of decimal place values removed from the tail, in case the repetition is delayed. i.e. 0.567777777...
        Do 'loop through start points for the repeating digits
            delay_digits = delay_digits + 1
            If delay_digits >= Fix(Len(tail_string_original) / 2) Then
                'Selection.TypeText Text:="Tried all starting points for the pattern, up to half way through the tail.  None was found.  I'll treat it as a terminating decimal." & vbCrLf
                GoTo treat_as_terminating
            End If
            num_removed = Mid(num, 1, decimalposition) & Mid(num, decimalposition + 1 + delay_digits) 'original number with decimal values removed
            tail_string_removed = Mid(num_removed, InStr(CStr(num_removed), ".") + 1)
            repeat_digits = 0 'exponent on 10 for moving the decimal place over
            'Selection.TypeText Text:="Searching " & num_removed & " for a pattern:" & vbCrLf
            Do
                repeat_digits = repeat_digits + 1
                If repeat_digits = Len(tail_string_removed) - 1 Or repeat_digits >= 9 Then 'try removing a digit, incase the pattern is delayed
                    Exit Do
                End If
                tail_string_test = Mid(num_removed, decimalposition + 1 + repeat_digits)
                'Selection.TypeText Text:=vbTab & "Comparing " & Mid(tail_string_removed, 1, Len(tail_string_removed) - repeat_digits) & " to " & tail_string_test & vbCrLf
                If Mid(tail_string_removed, 1, Len(tail_string_removed) - repeat_digits) = tail_string_test Then
                    'Selection.TypeText Text:=num & ", " & Mid(tail_string_removed, 1, Len(tail_string_removed) - repeat_digits) & " vs " & tail_string_test & vbCrLf
                    GoTo foundpattern
                End If
            Loop

        Loop 'next starting point for pattern

foundpattern:
        If delay_digits = 0 Then 'found pattern right away
            numerator = CLng(Mid(CStr(number), decimalposition + 1 + delay_digits, CInt(repeat_digits)))

            'generate the denominator nines, same number of digits as the numerator
            bottom = ""
            For i_loop = 1 To repeat_digits
                bottom = bottom & "9"
            Next
            denominator = CLng(bottom)
        Else 'there were numbers before the pattern began
            numerator = CLng(Mid(num, decimalposition + 1, delay_digits + repeat_digits)) - CLng(Mid(num, decimalposition + 1, delay_digits))
            'i.e. x = 2.73232323232...  delay_digits = 1, repeat_digits = 2, so numerator = 732 - 7 = 725
            bottom = ""
            For i_loop = 1 To repeat_digits
                bottom = bottom & "9"
            Next
            For i_loop = 1 To delay_digits
                bottom = bottom & "0"
            Next
            denominator = CLng(bottom)
            'i.e. 990...  725/990 = 145/198 = 0.7323232...
        End If



    Else ' terminating decimal
treat_as_terminating:
       'grab just the decimal trail
       If denom = -1 Then
            number = Math.Round(number, 8) 'reduce to fewer decimal places to avoid overload
             'is it a whole number now?
            If Fix(number) = number Then 'whole number
                output = CStr(number)
                GoTo all_done
            End If
            num = CStr(number)
            numerator = CLng(Mid(num, decimalposition + 1))
            denominator = 10 ^ (Len(num) - InStr(num, "."))
       Else 'express as a fraction rounded to the nearest denom'th reduced
            numerator1 = CDbl("0" & Mid(CStr(num), decimalposition))
            numerator = CInt(Math.Round(numerator1 * denom))
            denominator = CInt(denom)
       End If
    End If

    'reduce the fraction if possible using Euclidean Algorithm
    a = CLng(numerator)
    b = CLng(denominator)
    Dim t As Long
    Do While b <> 0
        t = b
        b = a Mod b
        a = t
    Loop
    gcd_ = a

    numerator = numerator / gcd_
    denominator = denominator / gcd_
    whole_part = CLng(Mid(num, 1, decimalposition - 1))


    'only write a whole number if the number is absolutely greater than zero, or will round to be so.
    If whole_part <> 0 Or (whole_part = 0 And numerator = denominator) Then
        'case where fraction rounds to whole
        If numerator = denominator Then
            'increase the whole by 1 absolutely
            whole_part = (whole_part / Abs(whole_part)) * (Abs(whole_part) + 1)
        End If
        output = CStr(whole_part) & " "

    End If

    'if fraction rounded to a whole, it is already included in the whole number
    If numerator <> 0 And numerator <> denominator Then
        'negative sign may have been missed, if whole number was -0
        If whole_part = 0 And number_ < 0 Then
            numerator = -numerator
        End If
        output = output & CStr(numerator) & "/" & CStr(denominator) & " "

    End If
    If whole_part = 0 And numerator = 0 Then
        output = "0"
    End If
all_done:
    If buildup = True Then 'build up the equation with a pretty fraction at the current selection range
        Dim objRange As Range
        Dim objEq As OMath
        Dim AC As OMathAutoCorrectEntry
        Application.OMathAutoCorrect.UseOutsideOMath = True
        Set objRange = Selection.Range
        objRange.Text = output
        For Each AC In Application.OMathAutoCorrect.Entries
            With objRange
                If InStr(.Text, AC.Name) > 0 Then
                    .Text = Replace(.Text, AC.Name, AC.Value)
                End If
            End With
        Next AC
        Set objRange = Selection.OMaths.Add(objRange)
        Set objEq = objRange.OMaths(1)
        objEq.buildup

        'Place the cursor at the end of the equation, outside of the OMaths object
        objRange.OMaths(1).Range.Select
        Selection.Collapse direction:=wdCollapseEnd
        Selection.MoveRight Unit:=wdCharacter, count:=1
        as_fraction = "" 'just a dummy return to make the function happy
    Else 'just return a flat string value
        as_fraction = output
    End If
End Function
查看更多
淡お忘
7楼-- · 2020-07-07 04:11

Similar to CookieOfFortune's, but it's in VB and doesn't use as much brute force.

Dim tolerance As Double = 0.1   'Fraction has to be at least this close'
Dim decimalValue As Double = 0.125  'Original value to convert'
Dim highestDenominator = 100   'Highest denominator you`re willing to accept'

For denominator As Integer = 2 To highestDenominator - 1
    'Find the closest numerator'
    Dim numerator As Integer = Math.Round(denominator * decimalValue)

    'Check if the fraction`s close enough'
    If Abs(numerator / denominator - decimalValue) <= tolerance Then
        Return numerator & "/" & denominator
    End If
Next

'Didn't find one.  Use the highest possible denominator'
Return Math.Round(denominator * decimalValue) & "/" & highestDenominator

...Let me know if it needs to account for values greater than 1, and I can adjust it.

EDIT: Sorry for the goofed up syntax highlighting. I can't figure out why it's all wrong. If someone knows how I can make it better, please let me know.

查看更多
登录 后发表回答