1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package Torello.Java;

import java.util.Iterator;
import java.util.Objects;
import java.util.function.Function;


// This does the "Tabbed Spacing" thing that the IDE does when you hit tab
// Note that passedBaseStr can be null
// 
// A "Leading String" or "Base String" is require knowledge because the tab compute function
// aligns the spacing with a column multiple of 4!

class TabbedIndentation
{
    static <PRINTABLE> Function<PRINTABLE, String> getFunction(
            final String                                passedBaseStr, 
            final Iterable<PRINTABLE>                   iterable,
            final Function<? super PRINTABLE, String>   passedGetStr,
            final byte                                  spacesPerTab
        )
    {
        if ((spacesPerTab < 2) || (spacesPerTab > 10)) throw new NException 
            ("The value passed to 'spacesPerTab' must be between 2 and 10");

        Objects.requireNonNull(iterable, "null has been passed to parameter 'iterable'");

        final String baseStr = (passedBaseStr != null)
            ? passedBaseStr
            : "";

        final Function<? super PRINTABLE, String> getStr = (passedGetStr != null)
            ? passedGetStr
            : (PRINTABLE p) -> ((p == null) ? null : p.toString());

        if (baseStr.length() > 100) throw new StringFormatException(
            "passedBaseStr has a length greater than 100 " +
            "(length=" + passedBaseStr.length() + ')'
        );

        if (baseStr.indexOf('\n') >= 0) throw new StringFormatException
            ("passedBaseStr contains a newline ('\\n') character.");

        final java.util.Iterator<PRINTABLE> iter = iterable.iterator();

        int max     = 0;
        int count   = 0;
        int first   = -1;

        boolean hasMoreThanOneLength = false;

        while (iter.hasNext())
        {
            count++;

            final PRINTABLE p = iter.next();

            // This is not actually the biggest deal, so I am leaving it off
            // As long as 'getStr' never returns null, we are OK.
            // 
            // Objects.requireNonNull
            //     (p, "The Iterator produced by parameter 'iterable' returned null");

            final String s = getStr.apply(p);

            Objects.requireNonNull
                (s, "One of the String's which has been returned by getStr's 'apply()' is null");

            final int l = s.length();

            if (l > 100) throw new StringFormatException(
                "The Iterator's " + count + StrPrint.ordinalIndicator(count) + " returned " +
                "String has a length greater than 100 (length=" + l + ")"
            );

            if (s.indexOf('\n') >= 0) throw new StringFormatException(
                "The Iterator's " + count + StrPrint.ordinalIndicator(count) + " returned " +
                "String contains a newline ('\\n') character."
            );

            // Standard calculation for computing the max
            if (l > max) max = l;

            // first is initialized to -1, so if it isn't -1, it has been assigned at least once
            if (first >= 0)
                { if (l != first) hasMoreThanOneLength = true; }
            else 
                first = l;
        }

        if (count == 0) throw new EmptyListException
            ("The Iterable's iterator did not return any values");


        // If every one of the returned strings had an identical length, then the printer will not
        // right space pad the returned strings to align with 'spacesPerTab'.  In such cases, only 
        // a single space character shall be appended to the end of the returned string
        // 
        // If you are wondering WTF I am talking about, or WTF this means - it is this minor 
        // inconvenient point:
        // 
        // int var1 = 1;
        // int var2 = 2;
        // int var3 = 3;
        // 
        // In the above code, since 'var1', 'var2' and 'var3' are variables whose name always have
        // the same length (a length of exactly four characters), there is no reason to align the equals
        // sign on a Tabbed-Column
        // 
        // This is when the equals-sign must align to the name of the variable, based on tabbed-value
        // according to 'spacesPerTab' (which in Torello Java-HTML is always 4)
        // 
        // int varName          = 1;
        // int someOtherVarName = 2;
        // int thisNameToo      = 3;

        if ((count == 1) || (! hasMoreThanOneLength))
        {
            // All of the lengths which were measured were equal to 'first'
            final int COMPUTED_LEN = first;

            return (PRINTABLE p) ->
            {
                final String s = getStr.apply(p);

                if (s == null)
                    throw new NullPointerException("'getStr' has returned null.");

                if (s.length() != COMPUTED_LEN) throw new StringFormatException(
                    "The returned String doesn't have length " +
                    '[' + COMPUTED_LEN + "], but rather [" + s.length() + ']'
                );

                return baseStr + s + ' '; 
            };
        }


        // These computations are valid for 'spacesPerTab' set to 4
        // ceiling(x / 4) * 4
        //
        // OLD VERSION (by chat-gpt)
        // final int rounded_to_mult_Of_4  = ((total + 3) / 4) * 4;
        // 
        // NEW VERSION, with explanation (also by chat-gpt)
        // final int rounded_to_mult_of_4  = (total + 3) & ~3;
        // 
        // ~3 in binary is:
        //      3    ==> 00000011
        //      ~3   ==> 11111100

        final int MAX   = max;                      // Effectively Final
        final int N     = spacesPerTab;             // Abbreviated for readability
        final int TOTAL = baseStr.length() + MAX;

        // final int rounded_to_mult_of_N  = ((TOTAL + (N - 1)) / N) * N;
        final int rounded_to_mult_of_N = ((TOTAL / N) + 1) * N;
        final int TABBED_SPACE_REQUIRED = rounded_to_mult_of_N - baseStr.length();

        return (PRINTABLE p) ->
        {
            final String s = getStr.apply(p);

            // This was already checked above, it seems a little bit "over zealous" to actually do 
            // the exact same check twice.  I mean, it's a little inefficient, wouldn't you say?
            // 
            // if (s.indexOf('\n') >= 0) throw new StringFormatException 
            //     ("'getStr' has returned a String with a newline ('\\n') character.");


            // This is here in case (for some odd reason) "they changed their mind" - and passed some 
            // other string which wasn't part of the original computation

            if (s == null) throw new NullPointerException
                ("'getStr' has returned null.");

            if (s.length() > MAX) throw new StringFormatException(
                "'getStr' has returned a String with a length greater than the oringal computed "+
                "maximum string length (Comuted-Max: " + MAX + ", Returned-Length: " + s.length() +
                ")"
            );

            final int SPACE = TABBED_SPACE_REQUIRED - s.length();

            if (SPACE < 1) throw new InternalError(
                '\n' +
                "s:                              " + '[' + s + "]\n" +
                "SPACE (computed spaces needed): " + SPACE + '\n' +
                "spacesPerTab (N):               " + spacesPerTab + '\n' +
                "passedBaseStr:                  " + '[' + passedBaseStr + "]\n" +
                "baseStr:                        " + '[' + baseStr + "]\n" +
                "MAX (max str length):           " + MAX + '\n' +
                "TOTAL:                          " + TOTAL + '\n' +
                "rounded_to_mult_of_N:           " + rounded_to_mult_of_N + '\n' +
                "TABBED_SPACE_REQUIRED:          " + TABBED_SPACE_REQUIRED + '\n'
            );
    
            return
                baseStr + s +
                StringParse.nChars(' ', SPACE);
        };
    }

}