fortran-lang / fprettify

auto-formatter for modern fortran source code
https://pypi.python.org/pypi/fprettify
Other
368 stars 73 forks source link

Request to vertically align code blocks via various characters #157

Open adrifoster opened 1 year ago

adrifoster commented 1 year ago

It would be great to have a way to programmatically vertically align code.

e.g.:

this:

  logical, intent(in) :: doalb       ! true if time for surface albedo calc
  real(r8), intent(in) :: nextsw_cday ! calendar day for nstep+1
  real(r8), intent(in) :: declinp1    ! declination angle for next time step
  real(r8), intent(in) :: declin      ! declination angle for current time step
  logical, intent(in) :: rstwr       ! true => write restart file this step
  logical, intent(in) :: nlend       ! true => end of run on this step
  character(len=*), intent(in) :: rdate       ! restart file time stamp for name

would become this:

  logical,          intent(in) :: doalb       ! true if time for surface albedo calc
  real(r8),         intent(in) :: nextsw_cday ! calendar day for nstep+1
  real(r8),         intent(in) :: declinp1    ! declination angle for next time step
  real(r8),         intent(in) :: declin      ! declination angle for current time step
  logical,          intent(in) :: rstwr       ! true => write restart file this step
  logical,          intent(in) :: nlend       ! true => end of run on this step
  character(len=*), intent(in) :: rdate       ! restart file time stamp for name

Is this possible?

nbehrnd commented 12 months ago

@adrifoster Based on your GitHub profile, I assume you have both access and familiarity with Python and Jupyter notebooks because the approach below is set for copy-paste into a cell of the later.

Conceptually, it reads the strings and splits them on defined keywords; it is naïve as in «(hopefully) easy to understand, while terribly inefficient» because each iteration of adjustment / padding the entries, every line is read twice: once to determine the maximal width in this «column», then to eventually adjust this column. I'm fine if you or/and an other provides a more efficient solution than this zeroth generation doodle:

def split_mechanism(raw_data = "", keyword = ""):
    """provide lines padded up to, yet excluding a particular keyword"""
    length_longest_fragment = 0
    adjusted_lines = []

    # the determination of the longest snippet length:
    for line in raw_data:

        length = len( str(line.split(keyword)[0]).strip() )
        if length > length_longest_fragment:
            length_longest_fragment = length

    for line in raw_data:
        # empty lines are not of our interest here:
        if len(line) == 0:
            continue

        padded_snippet = f"{line.split(keyword)[0].strip():{length_longest_fragment + 1}}"
        output = "".join([padded_snippet, keyword, line.split(keyword)[1]]) 
        adjusted_lines.append(output)

    return adjusted_lines

raw="""
logical, intent(in) :: doalb       ! true if time for surface albedo calc
real(r8), intent(in) :: nextsw_cday               ! calendar day for nstep+1
real(r8), intent(in) :: declinp1    ! declination angle for next time step
real(r8), intent(in) :: declin      ! declination angle for current time step
logical, intent(in) :: rstwr       ! true => write restart file this step
logical, intent(in) :: nlend       ! true => end of run on this step
character(len=*), intent(in out) :: rdate       ! restart file time stamp for name
"""

lines = raw.split("\n")

up_to_intent = split_mechanism(raw_data = lines, keyword = "intent")
up_to_double_colon = split_mechanism(raw_data = up_to_intent, keyword = "::")
up_to_comment = split_mechanism(raw_data = up_to_double_colon, keyword = "!")

for line in up_to_comment:
    print(line)

The input in the example differs twice from the example shared by you. In the second line, there is some additional white space. This aims to replicate situations when comments, attributes, variables etc. get removed / reorganized into other lines while assembling the source code. The white space is trimmed off to close the gaps; for easier reading of the next block however amended by one trailing single position. For instances of intent(out), or intent(in out) in lieu of intent(in), the last line of the input was edited accordingly.

With Python 3.11.5 / IPython 8.14.0 and Jupyter 6.4.12, the output of the cell above is

logical,          intent(in)     :: doalb       ! true if time for surface albedo calc
real(r8),         intent(in)     :: nextsw_cday ! calendar day for nstep+1
real(r8),         intent(in)     :: declinp1    ! declination angle for next time step
real(r8),         intent(in)     :: declin      ! declination angle for current time step
logical,          intent(in)     :: rstwr       ! true => write restart file this step
logical,          intent(in)     :: nlend       ! true => end of run on this step
character(len=*), intent(in out) :: rdate       ! restart file time stamp for name

which seems to provide the help intended.

Question to you: Out of curiosity, can you please indicate an example of this extended formatting? Part of the reason to write the slit_mechanism this way and its sequential calls is I sometimes encounter alignment in the declarations around :: only (independent of single spaces trailing/not trailing comma, single colon, etc.).

adrifoster commented 11 months ago

This is great thank you! In terms of an extended example... perhaps see this? https://github.com/ESCOMP/CTSM/blob/1e2e2c35d9568c94eac6cb606f37c18294158682/src/main/LandunitType.F90#L28 I agree that often the alignment is done in completely different ways, and I'm not sure what is the best.

nbehrnd commented 11 months ago

@adrifoster Thank you for indicating an example. As for a consistent format in Fortran in general, I'm not aware if there are recommendations this much formalized and frequently adopted as e.g., around PEP8 (and then checked / applied e.g., by flake8, pylint; black, yapf3, ruff) for Python.

Do not forget to protect the adjusted block, for example with a fence similar to

!&<
logical,          intent(in)     :: nlend       ! true => end of run on this step
character(len=*), intent(in out) :: rdate       ! restart file time stamp for name
!&>

Else, the next run of fprettify with its defaults (or an explicit style as defined in a style file indicated by -c, an example mentioned in a pending PR) is going to overwrite (and alter) it again.