Asked  8 Months ago    Answers:  5   Viewed   49 times

I'm currently trying to sort a multidimensional array by its subvalues. The structure of the array is:

[0] => Array
    (
        [id] => 87
        [sold] => 50
        [stock] => 991
        [speed] => 1.5
        [days_left] => 660.66666666667
    )

[1] => Array
    (
        [id] => 97
        [sold] => 20
        [stock] => 120
        [speed] => 1.2
        [days_left] => 100
    )

[2] => Array
    (
        [id] => 36
        [sold] => 2
        [stock] => 1020
        [speed] => 1.02
        [days_left] => 1000
    )

The code I'm using is:

usort($data, function($a, $b) { return $a[$_GET['sortby']] - $b[$_GET['sortby']]; });

where the $_GET['sortby'] variable equals the key.

So far so good, everthing is working, it sorts all values correctly EXCEPT the speed! First, I thought it has something to do with the decimal numbers, but the days_left include also decimals and are sorted correctly.. :/

Correct output (days_left):

[0] => Array
    (
        [id] => 97
        [sold] => 20
        [stock] => 120
        [speed] => 1.2
        [days_left] => 100
    )

[1] => Array
    (
        [id] => 87
        [sold] => 50
        [stock] => 991
        [speed] => 1.5
        [days_left] => 660.66666666667
    )

[2] => Array
    (
        [id] => 36
        [sold] => 2
        [stock] => 1020
        [speed] => 1.02
        [days_left] => 1000
    )

Wrong output (speed):

[0] => Array
    (
        [id] => 97
        [sold] => 20
        [stock] => 120
        [speed] => 1.2
        [days_left] => 100
    )

[1] => Array
    (
        [id] => 87
        [sold] => 50
        [stock] => 991
        [speed] => 1.5
        [days_left] => 660.66666666667
    )

[2] => Array
    (
        [id] => 36
        [sold] => 2
        [stock] => 1020
        [speed] => 1.02
        [days_left] => 1000
    )

Hope anybody can help me!

 Answers

60

See usort docs. Float result will be converted to integer. For correct work use this code:

usort(
    $data, 
    function($a, $b) {
        $result = 0;
        if ($a[$_GET['sortby']] > $b[$_GET['sortby']]) {
            $result = 1;
        } else if ($a[$_GET['sortby']] < $b[$_GET['sortby']]) {
            $result = -1;
        }
        return $result; 
    }
);
Wednesday, March 31, 2021
 
Zulakis
answered 8 Months ago
25

I've used a few things here, the main thing is that I use the sorting array as a target for an array_replace() and I use array_column() to index the objects by (this needs PHP 7.0+ to work with objects)...

$input = array_column($inputArray, null, "id");   
$sort = array_fill_keys($sortingArray, null);
$output = array_filter(array_replace($sort, $input));

The array_filter() will remove any elements which aren't in the input array but in the sorting array. The array_fill_keys() is used instead of array_flip() so that I can set a null value, which allows the filter to work.

Wednesday, March 31, 2021
 
aslum
answered 8 Months ago
44

I'm not familiar enough with the Decimal class to help you out, but your problem is due to the fact that decimal fractions can often not be accurate represented in binary, so what you're seeing is the closest possible approximation; there's no way to avoid this problem without using a special class (like Decimal, probably).

EDIT: What about the decimal class isn't working properly for you? As long as I start with a string, rather than a float, powers seem to work fine.

>>> import decimal
>>> print(decimal.Decimal("1.2") ** 2)
1.44

The module documentation explains the need for and usage of decimal.Decimal pretty clearly, you should check it out if you haven't yet.

Monday, June 14, 2021
 
Precastic
answered 5 Months ago
50

here an array_multisort example:

foreach ($array as $key => $node) {
   $timestamps[$key]    = $node[2];
}
array_multisort($timestamps, SORT_ASC, $array);
Tuesday, August 3, 2021
 
ALH
answered 3 Months ago
ALH
87

I've struggled quite a bit with this.

The best approach for me is to define a custom binder for decimals:

public class DecimalModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider
            .GetValue(bindingContext.ModelName);
        ModelState modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        try
        {
            //Check if this is a nullable decimal and a null or empty string has been passed
            var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
                                     string.IsNullOrEmpty(valueResult.AttemptedValue));

            //If not nullable and null then we should try and parse the decimal
            if (!isNullableAndNull)
            {
                actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
            }
        }
        catch (FormatException e)
        {
            modelState.Errors.Add(e);
        }

        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }
}

and bind it in the Application_Start in Global.asax:

ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());

I also use the globalize script (with cultures) which can be found here or downloaded from nuget here.

Your bundle should look something like this:

bundles.Add(ScriptBundle("~/bundles/jquery").Include(
    "~/Scripts/jquery-{version}.js",
    "~/Scripts/globalize.js",
    "~/Scripts/cultures/globalize.culture.en-GB.js",
    "~/Scripts/cultures/globalize.culture.it-IT.js"
    ));

Of course you can add more culures if you want to support different localizations.

Now, when your DOM is ready (javascript) you can define your culture:

Globalize.culture('en-GB');

$.validator.methods.number = function (value, element) {
    return this.optional(element) || jQuery.isNumeric(Globalize.parseFloat(value));
}

//Fix the range to use globalized methods
jQuery.extend(jQuery.validator.methods, {
    range: function (value, element, param) {
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (val >= param[0] && val <= param[1]);
    }
});

$.validator.methods.date = function (value, element) {
    return (this.optional(element) || Globalize.parseDate(value));
}

and customize your validations (I've added the date as well). You've done that in your jQueryFixes.

You can find a working example here (MvcAppForDecimals) where you can change languages using a toolbar and cookies so that the culture can change on the server as well.

In the example I read the cookie in Application_BeginRequest or use the default culture define in the web.config:

<globalization enableClientBasedCulture="true" uiCulture="en-GB" culture="en-GB" />

I've also defined a ActionFilter (LanguageFilterAttribute) which injects the current culture in a base viewmodel so the client uses the current set on the server side.

An extended explanation can be found here.

Some more info about the globalize script and culture settings here.

Tuesday, August 17, 2021
 
Teno
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :