简体   繁体   中英

Excel VBA not working well with C dll for decimals - works fine with integers

I have a problem that really stumps me. Ultimately, I think that the problem could very well be down to my lack of knowledge about C programming for Windows or how to properly work between C and VBA.

I have been dabbling in VBA and C for a while and I thought I would combine them for an Excel project since VBA does not execute very fast.

Sometimes when working in Excel I need to find a value in a long list of values. For one value it is easy (Ctrl+F), but sometimes the value I want can only be found by combining two or more values in the list.

For that reason I have written a macro that reads the values into an array and then loops through the array trying each value in combination with another to see if they combined become the value I seek. I have now moved the looping part into a dll written in C and that certainly has sped things up, but there is a problem: most of the time – but not always - it fails to find values that actually do combine into the value that I seek if the value I seek is a decimal value.

To try and find where the problem is, I have made the dll print all tested combinations and their results into a text file, in which I can see that there are matches, but for some reason my if statement does not trigger on it.

What could be the problem?

Here is my VBA code:

Private Declare Function FindVal Lib "mdvlib.dll" (ByRef dIn As Double, ByRef dOut As Double, ByVal iSizeIn As Long, ByVal sVal As Double, ByVal lvl As Long) As Long

Sub Match_Amounts(needle As Double, startcell As Range, level As Integer)

Dim haystack() As Variant
Dim i, j As Integer
Dim num As String
' dim variables going to the dll
Dim valArr() As Double
Dim valArr2() As Double
Dim arrSz, retval As Long

' read values from sheet into the array and find out its size
haystack() = Range(startcell, startcell.End(xlDown))
arrSz = UBound(haystack, 1)

're-dimension arrays that will be passed to the dll
ReDim valArr(1 To arrSz)
‘ using 100 here just to be on the safe side, will optimize later…
    ReDim valArr2(1 To arrSz * 100)
    ' assign values
    For i = 1 To arrSz
        valArr(i) = haystack(i, 1)
    Next
    ' change directory so that the macro finds the dll
    ChDir Application.UserLibraryPath
    ' use the FindVal function in the dll
    retval = FindVal(valArr(1), valArr2(1), arrSz, needle, level)

' present results
If retval > 0 Then
    j = PresRes(valArr2, level, retval)
Else
    num = Format(needle, "#,##0.00")
    'Then show a message to the user
    MsgBox "The value " & num & " could not be obtained by combining " & level & " values in the given range." _
    & vbCr & vbLf & vbCr & vbLf _
    & "This sometimes happens when searching for numbers with decimals. If this was the case, there could be values " _
    & "that combine to make up the sought number.", vbInformation, "Match Amounts"
End If

Erase haystack
Erase valArr
Erase valArr2

End Sub

The PresRes sub is just a way to present the results to the user and should not be relevant. However, please let me know should you want to see it.

My C code in the dll for the function that interacts with VBA is:

int __stdcall FindVal(double* dIn, double* dOut, int iSizeIn, double sVal, int lvl)
{
    if(lvl == 2) return FindValTwo(dIn, dOut, iSizeIn, sVal);
    if(lvl == 3) return FindValThree(dIn, dOut, iSizeIn, sVal);
    if(lvl == 4) return FindValFour(dIn, dOut, iSizeIn, sVal);

    return -1;
}

As can be seen above, I have written C functions for three different scenarios, for finding two, three or four addends but here I will show only the code for finding two values since that code is more compact and less complex than the others, and I have the problem in all of these functions.

Here is the code for the FindValTwo function:

int FindValTwo(double* dIn, double* dOut, int iSizeIn, double sVal)
{
    int i, j, k = 0;
    FILE *dumpfile = NULL;

    dumpfile = fopen("arraydump.txt", "a");

    for(i = 0; i < iSizeIn; i++){
        for(j = 0; j < iSizeIn; j++){
            fprintf(dumpfile, "%f + %f = %f (%f) [%d][%d]\n", dIn[i], dIn[j], dIn[i] + dIn[j], sVal, i, j);
            if(dIn[i] + dIn[j] == sVal && i != j){
                fprintf(dumpfile, "\t^ found and added!\n");
                if(ExistAlreadyTwo(dIn[i], dIn[j], dOut, k / 2) == 0){
                    dOut[k + 0] = dIn[i];
                    dOut[k + 1] = dIn[j];
                    k += 2;
                }
            }
        }
    }
    fclose(dumpfile);
    return k;
}

The lines above referring to file writing are there for debugging purposes and are not included otherwise. The code for the ExistAlreadyTwo function is:

int ExistAlreadyTwo(double needle1, double needle2, double* haystack, int l)
{
    // checks if the found values already exist in the return array
    int i, existalready = 0;

    for(i = 0; i < l; i++){
        if((needle1 == haystack[i * 2] && needle2 == haystack[i * 2 + 1]) || (needle1 == haystack[i * 2 + 1] && needle2 == haystack[i * 2])){

            existalready = 1;
            break;
        }
    }
    return existalready;
}

For testing, I made a simple array in Excel:

2.1
4.2
6.3
8.4
10.5
12.6

If I search for 21 I get a hit reporting that it is 8.4 and 12.6 that combine into 21. The text file also verifies this:

 8.400000 + 12.600000 = 21.000000 (21.000000) [3][5]
        ^ found and added!

and a bit further down:

12.600000 + 8.400000 = 21.000000 (21.000000) [5][3]
    ^ found and added!

However, when searching for a decimal value, eg 18.9, I do not get any hits, even though the file indicates that the values do exist and do combine into the sought value. Output from text file:

8.400000 + 10.500000 = 18.900000 (18.900000) [3][4]

And

10.500000 + 8.400000 = 18.900000 (18.900000) [4][3]

Since two decimal values are picked up if they combine into an integer, at first I did not think that the problem was in transferring the array to the dll but rather in transferring the value I am looking for.

However, I tried hard-coding the sought value in C, in the FindValTwo function, with the line:

sVal = 18.9;

… but that did not help either, it was not found. The text file looked exactly as above though.

I have tried both ByVal and ByRef (but only ByRef for the arrays), but I get the same results. I use 32-bit Excel 2010 (version 14.0.7163.5000).

I've read through the code

In VBA

 Dim i, j As Integer

will cause i to be a variant

Similarly: Dim arrSz, retval As Long

Also, from you code,

ReDim valArr2(1 To arrSz * 100)
' assign values
For i = 1 To arrSz
    valArr(i) = haystack(i, 1)
Next

I suspect you may not know ranges can be copied directly to an array in Excel See here FYI:

I'm suprised that you having to use C to speed this up, my gut tells me this should work quite fast in VBA, if coded well.

I'm also assuming that you have decided against using multiple columns of VLOOKUPS in your workbook, for performance reasons

I hope this may be of help, sorry I didn't find the bug, it sounds like a data type issue, so

Harvey

So, I will just let everyone know what I did to make it work.

The problem was, as Paul Ogilvie kindly pointed out, in the comparison of floating point numbers, so the problem was in my C code all along.

I did calculate the epsilon but for some reason I could not make that work in this context. I will pursue that at a later time, but for now I just settled for an acceptable error of 0.000000001. So, from the code I provided in my question, I changed my FindValTwo function. The new function now looks like this:

int FindValTwo(double* dIn, double* dOut, int iSizeIn, double sVal)
{
    int i, j, k = 0;
    double aErr = 0.000000001; // acceptable error (for still calling it a match)

    for(i = 0; i < iSizeIn; i++){
        for(j = 0; j < iSizeIn; j++){
            if(fabs(dIn[i] + dIn[j] - sVal) < aErr && i != j){
                if(ExistAlreadyTwo(dIn[i], dIn[j], dOut, k / 2) == 0){
                    dOut[k + 0] = dIn[i];
                    dOut[k + 1] = dIn[j];
                    k += 2;
                }
            }
        }
    }
    return k;
}

I have also gone through all of my VBA code and changed the variable declarations after learning that I had not been doing them correctly. However those changes should be pretty obvious from above so I will not show them here.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM