Number vs Float discrepancy between simulator and device

I’ve run into a weird issue and would like your opinions.

I have code that performs some calculations that should produce a Number, and then instantiates an object whose constructor expects a Number.

The compiler seems happy that these calculations result in a Number and allows me to pass the result into the constructor. Later, I store that value in a member typed as Object, and later convert it back to a Number again. At that point I had a safety instanceof check to confirm the Object really is a Number.

In the simulator everything works fine, but on some real devices (under some conditions) it looks like the calculation results in a Float. Monkey C “duck typing” then lets the Float propagate without complaint until my explicit check, which is how I noticed.

To me the core issue is that the compiler believes the code results in a Number, the simulator behaves accordingly, but real devices sometimes behave differently.

Here is the relevant code (with comments):

var minValue = sitemapNumeric.getMinValue(); // returns a Number
var maxValue = sitemapNumeric.getMaxValue(); // returns a Number
var step     = sitemapNumeric.getStep();     // returns a Number

var currentValue =
    numericItem.hasState()
    ? numericItem.getNumericState() // returns a Number
    : (minValue + Math.round((maxValue - minValue) / (2 * step)) * step).toNumber();

for (var i = minValue; i <= maxValue; i += step) {
    if (_currentIndex == -1 && currentValue <= i) {
        _currentIndex = _pickables.size();
        if (i != currentValue) {
            _nonConforming = new NumericPickable(currentValue, unit); // expects Number
            _pickables.add(_nonConforming);
        }
    }
    _pickables.add(new NumericPickable(i, unit)); // expects Number
}

And here is NumericPickable:

class NumericPickable extends CustomPickable {
    public function initialize(value as Number, unit as String) {

        // Workaround: force it to be a Number, so later reads are consistent
        value = value.toNumber();

        // CustomPickable stores the first parameter as Object
        CustomPickable.initialize(value, value.toString() + unit);
    }
}

Has anyone seen something like this before?

Once I understand what’s going on, the workaround is easy (the extra toNumber() in NumericPickable). But I’d really like to understand why the compiler and simulator treat this as a Number, while some real devices end up producing a Float.

If anyone is interested, here are the full source files. The first file contains the calculation discussed above, starting at line 39:

https://github.com/openhab/openhab-garmin/blob/main/source/user-interface/control-views/numeric/NumbericPickerFactory.mc

https://github.com/openhab/openhab-garmin/blob/main/source/user-interface/control-views/numeric/NumericPickable.mc

  • I haven’t been able to reproduce the issue on my own watch, even when using the exact same JSON as the affected user. That said, I have to mock the server using Postman, so there’s a possibility that the payload delivery differs slightly from the real server. Because of this, I can’t say with absolute certainty whether my watch is truly unaffected or whether the issue simply doesn’t occur due to differences in the mock setup.

    To investigate further, I’ve rewritten the JSON accessor to perform strict type checking. It now verifies that the value is a Number; if not, it explicitly checks the other numeric types as well as String and raises an error that reports the actual type encountered.

    This change is intended for testing only. In the final implementation, the accessor will be made more resilient and accept all types that can be safely converted to a Number.

  • There's an even "better" way:

    This was also my first attempt at making the code more resilient, but with strict type checking it doesn’t compile. I hit “cannot determine type for method invocation”. The compiler still requires a concrete type cast to call it, e.g.:

    Since I also want to handle non-convertible strings (where toNumber() returns null), I switched to explicit type checks and conversion.

  • Also have you ever really tried: x instanceof Numeric?
    Does it really work? Because Numeric is not a type, it's a typedef:

    Yes, unfortunately that doesn’t work. I’m using typedef for more complex types, for example:

    typedef JsonObject as Dictionary<String, Object?>;

    In this case, instanceof JsonObject does not work at all. Additionally, the check can only be performed against Dictionary itself, not against the more specific Dictionary<String, Object?>.

  • Again this casting? I thought flowsate already explained it!

    This works with strict typecheck:

    function toConfigNumber(value as PropertyValueType?, defaultValue as Number) as Number {
    if (value instanceof Lang.Boolean) {
      return value ? 1 : 0;
    }
    // this should cover Float, Double, Number, String, Long, Char, Symbol
      value = value != null && value has :toNumber ? value.toNumber() : null;
      return value != null ? value as Number : defaultValue;
    }
  • yeah, but I wouldn't even expect this to work in other languages. For example Java also deletes the generic type (type erasure)

  • Again this casting? I thought flowsate already explained it!

    Which casting are you referring to?

    This works with strict typecheck:

    Interesting. For me it does not:

  • add

    import Toybox.Application.Properties;
    import Toybox.Lang;
  • (value as Float).toNumber()

    I’m not actually using that approach; it was only meant to demonstrate that has doesn’t work for me, even though it appears to work in your case. Without the cast, I can’t call toNumber(), even after explicitly checking for its existence with has.

    The code I’m currently using is the longer version, which avoids casts entirely.

  • The test performed by the affected user has confirmed that the issue is caused by how the JSON is read. Specifically, the value "5" for the step is being interpreted as a Float in his environment.

    The code posted above, which explicitly handles all numeric types, resolves the problem and is a much cleaner solution than my initial workaround.

    As for why I couldn’t reproduce the issue in the simulator or on my own watch, it appears that the mock-up is indeed the reason. I had the user test the mock-up on his watch, and that works correctly as well. The issue of the value being delivered as Float only occurs when using his real server. I still haven’t identified what exactly in the server response causes this behavior. From ERA, I know that others have encountered the same issue, but when I try to replicate his configuration on my own server, everything works as expected and the "5" is delivered as Number.

    I’ll continue to look into it out of curiosity, but since the fix is robust, I consider the issue effectively resolved.

    Thanks again for your help. I wouldn’t have figured this out without you. ThumbsupBlush