Skip to content

Simplify the to_string implementations in the various units formats - #14418

Merged
pllim merged 6 commits into
astropy:mainfrom
mhvk:units-format-simplify-to-string
Feb 21, 2023
Merged

pllim merged 6 commits into
astropy:mainfrom
mhvk:units-format-simplify-to-string

Conversation

@mhvk

@mhvk mhvk commented Feb 19, 2023

Copy link
Copy Markdown
Contributor

Description

This pull request does as it says, mostly by avoiding separating out paths based on the unit class, since the more complicated path will work for all cases anyway. It is split off from #14416 to make review a bit easier.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see "When to rebase and squash commits".
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • Is a milestone set? Milestone must be set but we cannot check for it on Actions; do not let the green checkmark fool you.
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@nstarman nstarman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some questions, which maybe indicates some explanatory comments will be helpful for future maintainers.
Otherwise, I think this PR is good, especially given that no tests change.

s = ""
if unit.scale == 1:
s = ""
else:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In personal use I haven't noticed a case when unit.scale != 1. From context I'm guessing this happens for exponentials, but more specifically... when is the scale not 1? Maybe a comment is warranted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You get it most often by decomposing. E.g.,

u.Ry.decompose()
# Unit("2.17987e-18 kg m2 / s2")

(the number is the scale).

else:
s = cls.format_exponential_notation(unit.scale)

if len(unit.bases):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried bool(len(u.m.bases)) and this evaluates to True. When is len(unit.bases) False?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only for dimensionless - which can have a scale:

u.percent.decompose()
# Unit(dimensionless with a scale of 0.01)
f"{u.percent.decompose()}"
# 0.01

Comment thread astropy/units/format/console.py Resolved
@mhvk
mhvk force-pushed the units-format-simplify-to-string branch from 0202e91 to 6d870be Compare February 20, 2023 22:08
@mhvk

mhvk commented Feb 20, 2023

Copy link
Copy Markdown
Contributor Author

Rebased and adopted your suggestion -- but didn't add any comments yet. Let me know if having seen my answers, you still think it is useful.

@nstarman

Copy link
Copy Markdown
Member

Let me know if having seen my answers, you still think it is useful.

I think even just including the answers as comments would go a long way!

@mhvk

mhvk commented Feb 20, 2023

Copy link
Copy Markdown
Contributor Author

OK, you're right, comments that explain what would happen are really always good. Added!

@mhvk mhvk mentioned this pull request Feb 21, 2023
4 tasks done
else:
nominator = cls._format_unit_list(nominator)
s += nominator

@olebole olebole Feb 21, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is very similar with what is in console.py and generic.py, couldn't this also united? By having a format_fraction(nominator, denominator) method that takes the specifics of console and latex?

class Generic(Base): […]
    @classmethod
    def to_string(cls, unit, inline=False):
        parts = []
        if unit.scale != 1:
            parts.append(cls.format_exponential_notation(unit.scale))

        if len(unit.bases):
            if inline:
                nominator = zip(unit.bases, unit.powers)
                denominator = []
            else:
                nominator, denominator = utils.get_grouped_by_powers(
                    unit.bases, unit.powers
                )
            if len(denominator):
                if not len(nominator):
                    nominator = ["1"]
                parts.append(cls._format_fraction(nominator, denominator))
            else:
                parts.append(cls._format_unit_list(nominator))

        return ' '.join(parts)

    @classmethod
    def _format_fraction(nominator, denominator):
        if len(denominator == 1):
            return f"{cls._format_unit_list(nominator)} / {cls._format_unit_list(denominator)}"
        else:
            return f"{cls._format_unit_list(nominator)} / ({cls._format_unit_list(denominator)})"


class Latex(Generic): […]
    @classmethod
    def _format_fraction(nominator, denominator):
        return rf"\frac{{{cls._format_unit_list(nominator)}}}{{{cls._format_unit_list(denominator)}}}"

    @classmethod
    def to_string(cls, unit, inline=False, wrap=True):
        s = super().to_string(unit, inline)
        if wrap:         # properly wrap into LaTeX mathmode
            return rf"$\mathrm{{{s}}}$"
        else
            return s


class Console(Generic): […]
    @classmethod
    def _format_fraction(nominator, denominator):
        nominator = cls._format_unit_list(denominator)
        denominator = cls._format_unit_list(denominator)
        fraclength = max(len(nominator), len(denominator))
        f = f"{{0:<{len(s)}s}}{{1:^{fraclength}s}}"

        return "\n".join(
            (
                f.format("", nominator),
                f.format(s, cls._line * fraclength),
                f.format("", denominator),
            )

    @classmethod
    def to_string(cls, unit, inline=True):
        return super().to_string(unit, inline)

This would concentrate all the handling of the different options (with/without de/nominator, inline) in a general re-usable to_string() method and have the inline parameter more general. And for the LaTeX one has the easy option to switch off the wrapping, which could remove the hack of later removing it when a Quantity is formatted.

Or does this get too abstract then?

Update: By re-checking, I see that one would then somehow need to handle the multi-line concatenation of scale and base unit in generic.py instead of just joining the two parts here. For this, the part included now in Quantity formatting could be re-used; however this further complicates the structure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, those were just my thoughts, and indeed the next step includes moving to_string() -- see #14416! (move to Base, since console, etc., shouldn't inherit the parsing form generic).

Aside, while so far (including #14416) the tests do not change, I have been thinking that perhaps, while we still can, we should rename inline -- it made sense for latex but I feel it is somewhat unclear for other formats, and it would also be nice to separate in-lline fractions from using negative powers. The best I could come up with so far is fraction=False, fraction='small' and fraction='big' (with fraction=True whatever makes most sense). Hopefully to extend to fits and ogip, where there is a wish to allow things like "Jy/beam" (see #13217).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

unit.bases, unit.powers
)
if len(nominator):
parts.append(cls._format_unit_list(nominator))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why some of the class methods are public (cls.format_exponential_notation()) and others are marked as private (cls._format_unit_list()). Unless there is a specific reason here, shouldn't the format functions all have the same visibility?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is well before my time, but I think the idea was that the public methods are the only ones also used outside of the class - e.g., format_exponential_notation is not used only for the scale, but also by Quantity to format the values.

Maybe something to reconsider, though! (Perhaps best after finishing the refactor.)

@nstarman nstarman left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mhvk! I think this PR is good to go and I like the plan for the PR sequence, including code simplification by refactoring console and generic.

@pllim pllim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am approving by proxy. I am not sure why Ole doesn't get green check mark. I have inquired.

@pllim
pllim merged commit 56a3a7c into astropy:main Feb 21, 2023
@pllim

pllim commented Feb 21, 2023

Copy link
Copy Markdown
Member

Thanks, all!

@mhvk
mhvk deleted the units-format-simplify-to-string branch February 21, 2023 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants