4

ToString (and thus Export) has a tendency to mangle the nice structure of Mathematica expressions as they are generally coded. This can be nice for compactness, but makes updating formatted .m files a nightmare.

Is there a nice way to get better formatting?

b3m2a1
  • 46,870
  • 3
  • 92
  • 239

1 Answers1

5

As usual, the answer is yes. And even better, it's relatively easy.

First we'll leverage the very helpful GeneralUtilities`PrettyForm. It is implemented in terms of a function GeneralUtilities`PrettyFormBoxes that we can use to directly construct the FE boxes of the expression. And even better we can use this to set the max line width (which in general I'll set to 1 to get full unwrapping).

It puts a bit of unnecessary formatting on that we'll strip off, but other than this it's pretty direct from the boxes to the string through FrontEnd`ExportPacket:

prettyString[expr_,
   elementsPerLine : _?IntegerQ : 1,
  dataType : TextData | BoxData : BoxData,
  cellStyle : _String : "Input",
  ops : OptionsPattern[Cell]
  ] :=
 StringDelete[
  FrontEndExecute[
    FrontEnd`ExportPacket[
     Cell[
      dataType@
        GeneralUtilities`PrettyFormBoxes[expr, elementsPerLine] /.

           TemplateBox[
         {sym_, ___},
         "DefinitionSymbol", 
         ___
         ] :> sym, 
      cellStyle,
      ops,
      PageWidth -> Infinity
      ],
     "PlainText"
     ]
    ][[1]],
  "\\" ~~ "\n"
  ]

And making something that'd be a bit annoying to format through some clever recursive scheme.

assoc =
  With[{a = AssociationThread[RandomWord[10], RandomReal[{}, 10]]},
   Append[a, 
    "a" -> Append[Take[a, 3], 
      GeneralUtilities`PrettyFormBoxes -> Take[a, -3]]]
   ];

Then looking at the relative results:

ToString[assoc, InputForm]

"<|\"impotence\" -> 0.7912604322028038, \"enfeeble\" -> \
0.5739318538887597, \"untangling\" -> 0.7293180075252037, \
\"stitched\" -> 0.9186439937018753, \"picaresque\" -> \
0.12956430307141908, \"reinterpret\" -> 0.011950593964761058, \
\"wheedler\" -> 0.20784766900413043, \"convulse\" -> \
0.38142965804171935, \"acquisitive\" -> 0.9234994949102955, \"oration\
\" -> 0.59873274050806, \"a\" -> <|\"impotence\" -> \
0.7912604322028038, \"enfeeble\" -> 0.5739318538887597, \
\"untangling\" -> 0.7293180075252037, \
GeneralUtilities`PrettyFormBoxes -> <|\"convulse\" -> \
0.38142965804171935, \"acquisitive\" -> 0.9234994949102955, \"oration\
\" -> 0.59873274050806|>|>|>"

prettyString[assoc]

"<|
    \"impotence\" -> 0.7912604322028038,
    \"enfeeble\" -> 0.5739318538887597,
    \"untangling\" -> 0.7293180075252037,
    \"stitched\" -> 0.9186439937018753,
    \"picaresque\" -> 0.12956430307141908,
    \"reinterpret\" -> 0.011950593964761058,
    \"wheedler\" -> 0.20784766900413043,
    \"convulse\" -> 0.38142965804171935,
    \"acquisitive\" -> 0.9234994949102955,
    \"oration\" -> 0.59873274050806,
    \"a\" -> <|
        \"impotence\" -> 0.7912604322028038,
        \"enfeeble\" -> 0.5739318538887597,
        \"untangling\" -> 0.7293180075252037,
        GeneralUtilities`PrettyFormBoxes -> <|
            \"convulse\" -> \
0.38142965804171935,
            \"acquisitive\" -> 0.9234994949102955,
            \"oration\" -> 0.59873274050806
        |>
    |>
|>"

The latter is, at least to me, much more readable.

And since we're just using the FE for this we can play with the FE options to easily do things like:

prettyString[assoc, 
 "Output",
 ExportAutoReplacements -> {
   "<" -> "(-", ">" -> "-)",
   "|" -> ":", " " -> "\t", 
   "\[Rule]" -> "-->"
   }
 ]

"(-:
    impotence   --> 0.7912604322028038,
    enfeeble    --> 0.5739318538887597,
    untangling  --> 0.7293180075252037,
    stitched    --> 0.9186439937018753,
    picaresque  --> 0.12956430307141908,
    reinterpret --> 0.011950593964761058,
    wheedler    --> 0.20784766900413043,
    convulse    --> 0.38142965804171935,
    acquisitive --> 0.9234994949102955,
    oration --> 0.59873274050806,
    a   --> (-:
        impotence   --> 0.7912604322028038,
        enfeeble    --> 0.5739318538887597,
        untangling  --> 0.7293180075252037,
        GeneralUtilities`PrettyFormBoxes    --> (-:
            convulse    --> 0.38142965804171935,
            acquisitive --> 0.9234994949102955,
            oration --> 0.59873274050806
        :-)
    :-)
:-)"

And finally this works in the same way as it always has with ToString:

prettyString@
 Unevaluated@prettyString[
   assoc, 
   "Output",
   ExportAutoReplacements -> {
     "<" -> "(-", ">" -> "-)",
     "|" -> ":", " " -> "\t", 
     "\[Rule]" -> "-->"
     }
   ]

"prettyString[
    assoc,
    \"Output\",
    ExportAutoReplacements -> {
        \"<\" -> \"(-\",
        \">\" -> \"-)\",
        \"|\" -> \":\",
        \" \" -> \"\\t\",
        \"->\" -> \"-->\"
    }
]"

Keep in mind, though, that this is much, much slower and should only be used when nice formatting is necessary:

prettyString@assoc // RepeatedTiming // First

0.05

ToString[assoc, InputForm] // RepeatedTiming // First

0.000093
b3m2a1
  • 46,870
  • 3
  • 92
  • 239
  • I found this while I was looking for a way to set a page width for export pacet but it does not seem to be respected at all. InputText always breaks and plain text does not. I'd like to use input text but I can't controll the width @dailystruggle. Any advice? – Kuba Nov 27 '18 at 09:22
  • @Kuba unfortunately not really. One option I do know about which feels as if it should be related is ExportMultipleCellsOptions. I had the "PageWidth" of that set below Infinity for a bit and it broke everything because everything tried to export to that width. – b3m2a1 Nov 27 '18 at 09:36