Refactor handling of number format

Issue #610 closed
Pol Welter created an issue

Currently the desired format of a number type is saved as a single char inside each type. Especially now that we have three types that each save the same (!) char, this is unsatisfactory. This we could be solved by simply removing the char format attribute from HNumber and CNumber.

However, as of issue #586, it becomes clear that a single char will not be up to the task. For instance, a complex number could be formatted as binary, fixed point, and polar all at the same time. The current system limits us to choose one option only.

I propose to add the necessary infrastructure to deal with this more properly.

Now, I do not have a very clear vision of how to do it, so that's why I'm asking. Currently I envision something similar to this:

  • First, replace the char by an enum.
  • Each number type offers its own enum of possible formats that can be passed to its format function. For instance, HMath might allow

    • GeneralDecimal
    • FixedDecimal
    • ScientificDecimal
    • EngineeringDecimal
    • GeneralBinary
    • GeneralHexadecimal
    • GeneralOctal
  • CMath::format on the other hand requires additionally either Cartesian and Polar.

I know there were some concerns about storing the format inside the actual number types. Why exactly is that a problem? Would it help if we stored everything in Quantity only?

Marking it as milestone as I see it a requirement for #586.

Comments (14)

  1. Felix Krull

    Well, storing the representation of the number with its value is a bit icky. I'd still prefer it if the format existed on a level above Quantity (the elusive Value story), but for the moment, I feel the format should only be stored in Quantity itself, yes. I played with that a bit on the number refactor PR, I think? It was pretty easy, the biggest issue was encodeIeee754 since it sets its result to hexadecimal; if HNumber loses its format, that action has to be moved either into DMath or functions.cpp directly.

    From the example in your second paragraph, are you suggesting to split the number base from the decimal format so e.g. you could have a number formatted as engineering binary? I can't really tell if that's something that makes sense, but if we can do that, we absolutely should.

    First, I agree that the formats should be specified as enum values in some fashion. Now. Due to how the number stack is structured, CMath::format needs to know both the complex number format as well as the "real" parts of the format so it can pass that to HMath::format. Accordingly, the format argument for these functions needs to be some value that encapsulates two or three distinct pieces of information. CMath::format pulls out the piece that specifies either cartesian or polar and acts on it, then passes the format value to its HMath::format calls.

    I've sketched some more concrete ideas; option a), distinct enums and a simple struct:

    enum RealFormat {
        GeneralDecimal,
        ...
    }
    enum ComplexFormat {
        Cartesian,
        Polar
    }
    struct Format {
        RealFormat realFormat;
        ComplexFormat complexFormat;
    };
    

    Option b), mush the different formats into a single value with some bit mangling, reserving some bits for "real" formats and some further bits for "complex" formats:

    enum Format {
        GeneralDecimal, //  = 0
        ...
        Cartesian = 0x100,
        Polar, // = 0x101
    
        // SomeFancyOtherFormat = 0x10000,
    }
    
    GeneralDecimalAndPolar = Format::GeneralDecimal | Format::Polar;
    if (fmt & Format::Polar) // check if polar form
    switch (fmt & 0xFF00) { // keep only the "complex" bits so we can switch on then:
        case Format::Polar: ...
    
  2. Pol Welter reporter

    Thanks for taking the time of writing this.

    From the example in your second paragraph, are you suggesting to split the number base from the decimal format so e.g. you could have a number formatted as engineering binary? I can't really tell if that's something that makes sense, but if we can do that, we absolutely should.

    Exactly. I don't know if this will be possible (or even useful), but now we can't even tell!

    I've also come to the conclusion of storing everything in Quantity is the way to go. Option a) seems much more appalling to me. Come to think, I imagine a class for each type's formatting might be useful.

    struct HNumber::Format
    {
        enum base {Binary, Decimal, Octal, Hexadecimal}; // possibly sexagesimal
        enum radixChar {Point, Comma};
        unsigned int precision;
        enum notation {General, Fixed, Scientific, Engineering};
    }
    
    struct CNumber::Format : HNumber::Format
    {
        enum notation {Cartesian, Polar};
    }
    
    struct Quantity::format : CNumber::Format
    {
        // Maybe unit & unitName should go here? They also only serve display purposes.
        CNumber unit;
        QString unitName;
    }
    

    Note that I think real and imaginary parts should really have the same format. In polar representation there cannot even be such a distinction anymore!

    Also note that not necessarily all possible combinations must be honored. If it turns out that binary+engineering (which seems hardly useful) is difficult to implement, the number is just formatted in whichever way makes most convenient (for the user, not the programmer :) instead.

    I'd consider adding an Unspecified key to each enum, which'd serve as default, and tell the numberformatter to pick the value from the settings.

    While the classic bitwise magic immediately allows for this very nifty format = Binary + Polar style of programming, I am afraid it won't cut the complexity we see here. Just look at the code snippet I just wrote. How should that possibly fit inside an integer? ;)

  3. Felix Krull

    Hm. Yeah OK, having a class or several for the format seems worthwhile.

    Pretty sure neither unit nor name can be in the format. Changing the format on a Quantity shouldn't change the effective value of the number, but that's what changing the unit does; ergo the unit needs to be in the number, not the format. And as for the unit name, the number still needs to print properly "without" a format, i.e. with an empty, "default" format value. So the number needs to know its unit and unit name without the format. The format could have a field to overwrite the unit name for display, but I'm not sure where that'd be useful.

    So thinking out loud:

    • Conceptually, a "format" has a bunch of properties, each of which can be either a particular value or unspecified/empty/null.
    • Every Quantity has a format, which can be as unspecified or as particular as it wants.
    • There is an application-wide "default format" that is constructed from user settings in some fashion. In practice, it probably won't have unspecified fields, but I don't think that's strictly necessary.
    • At some point, in numberformatter I guess, we need to merge the default format with the number's format, filling every unspecified field in the number's format with the default format's value.
    • The number stack all the way down to HMath::format should still be able to cope with "unspecified" values and use some sensible default.
  4. Pol Welter reporter

    Changing the format on a Quantity shouldn't change the effective value of the number, but that's what changing the unit does

    Not really. The dimension is part of the 'value', but the unit really is only a display thing. I agree though that we better keep it out of the format.

  5. Pol Welter reporter

    I have started to work on this. Turns out decoupling base from mode (e.g scientific vs fixed) is indeed doable. Win!

    However, I fail to see what the Fixed Decimal mode does, except from raising the limit upon which the representation is silently converted to scientific notation. Shouldn't it rather be used for enforcing a certain number of digits, possibly trailing zeros?

    For instance:

    0.12
    = 0.1200
    

    I guess this would be useful for finance guys. EDIT: Nvm, I have just figured out that that's what happens if you select a precision other than Auto.

  6. Pol Welter reporter

    Hm, actually my point still stands. Can somebody tell me the difference between Fixed and General? Really the only difference I can distinguish is that Fixed is much more conservative with when it switches to scientific notation.

    In other news, I have implemented the refactor as discussed. The only thing that is not working yet is that numberformatter fetches default formatting from the settings. Point is, the engine does indeed support providing the base and the mode (scientific, ...) independently. So I see two paths we can go from here:

    • Keep the menu/shortcuts/etc the same they are. If anyone wants to output a binary in scientific mode, we'll have to make a new scientific function.
    • Change the menu, so that the user can independently select base and mode. This will require new keyboard shortcuts.

    Since I really doubt the new combinations will be useful, I think I'll go with the first method.

    EDIT: Done. Will remake the tests, and then submit a PR.

  7. Tey'

    I believe "Fixed" means something like "represent the numbers without using exponent as much as possible". The behavior depends on the number of digits that can be shown (precision), and it is somehow equivalent to fixed point representation. "General" is like an automatic representation mode which mixes "Fixed" and "Scientific" representations depending on the exponent value.

    Indeed, the behavior of "Fixed" and "General" look the same if you consider it's only a matter of exponent values, but the fact the "Fixed" representation fallbacks to "Scientific" when it can not format the result without exponents is more a limitation than a feature of that mode.

    TBH, I don't really see the point of the "General" mode, but use the "Fixed" mode a lot when not doing scientific calculations.

    Since I really doubt the new combinations will be useful, I think I'll go with the first method.

    It can be useful when studying floating point numbers representation in hexa/binary, but that's only something you do in school anyway :)

  8. Helder Correia repo owner

    This is all legacy stuff and TBH I've always wanted to reformulate those settings (that have existed almost from day 1 and predate my presence in the project). Feel free to propose changes. I believe some of you are somehow related to electronics and may be aware of what makes more sense here.

  9. Pol Welter reporter

    I put some more thought into it, and I really think that Scientific binary is of no use whatsoever. If we would separate base and mode in the menu, than a user who wants to switch from general decimal to fixed binary, would need to flip two switches, rather than one at the moment.

    We could of course keep the keyboard shortcuts the way they are (for instance F2: general decimal, F6: fixed binary, etc.), essentially executing macros. But I see no reason for why the shortcuts should give different functionalities than the menu.

    Finally, we could implement scientific(), fixed() and engineering() functions, to complement hex() and friends. That's actually a very flexible yet convenient approach.

    TL;DR: Let's keep it the way it is. It is actually well thought out.

    As a last comment: Binary scientific notation is a concept not that widely known to even exist. I am very much afraid that it will confuse many more people than it will be used by.

  10. Helder Correia repo owner

    Yes, I'd like to have e.g. eng(). It could also mimic the behavior of the ENG (and ENG⁻¹ or Shift+ENG) key in classic calculators.

  11. Felix Krull

    Yeah, the current settings seem reasonable to me. Correct me if I'm wrong, but bases != 10 are pretty much exclusively used to cope with the love computers have for base-2; and when you're looking at numbers at that level, you're mostly dealing in integers anyway. Having it as a "hidden" feature by combining e.g. bin and scientific seems fair, but I think I'm ok with the current menu contents.

  12. Log in to comment