edit: I have posted a rags-to-riches question here here with an improved solution, do check it out too.
TRAITS
is a similar take on the array-based lookup approach suggested in @Antot @Antot's answer, except that it is String[][]
-based and contains the "X"
values for easier lookups when the percentage is exactly 50%, i.e. THRESHOLD
. BLANK
represents a Map
where there are no responses for a given choice.
edit: I have posted a rags-to-riches question here with an improved solution, do check it out too.
TRAITS
is a similar take on the array-based lookup approach suggested in @Antot's answer, except that it is String[][]
-based and contains the "X"
values for easier lookups when the percentage is exactly 50%, i.e. THRESHOLD
. BLANK
represents a Map
where there are no responses for a given choice.
edit: I have posted a rags-to-riches question here with an improved solution, do check it out too.
TRAITS
is a similar take on the array-based lookup approach suggested in @Antot's answer, except that it is String[][]
-based and contains the "X"
values for easier lookups when the percentage is exactly 50%, i.e. THRESHOLD
. BLANK
represents a Map
where there are no responses for a given choice.
edit: I have posted a rags-to-riches question here with an improved solution, do check it out too.
edit: I have posted a rags-to-riches question here with an improved solution, do check it out too.
Returning results from methods
Most of your processing methods except for computePersonality()
mutate the method arguments as a way of passing the results back to the method caller. There is nothing inherently wrong with this approach, but it does mean you have to be careful with the ordering of your method arguments (since your results are just int[]
arrays), and you will not be able to chain method calls together.
In some cases, it might be better to use a payload class, even if it's a custom one, to better model a result from a method. For example, instead of passing around two int[]
arrays, you can have a Result
class that provide methods like updateA(int[])
or computePercentage()
to better encapsulate such functionality.
Enumerating and validating inputs
Since your processing is only done on A
or B
(case-insensitive) values, an enum
may be a suitable option of representing these choices:
enum Choice {
A, B, UNKNOWN;
public static Choice of(char x) {
try {
return Choice.valueOf(String.valueOf(x).toUpperCase());
} catch (IllegalArgumentException e) {
return Choice.UNKNOWN;
}
}
}
In this case, we 'convert' "A"/"a"
as Choice.A
, and "B"/"b"
as Choice.B
. Other values are simply represented as Choice.UNKNOWN
.
Computing and grouping scores
Based on my interpretation of your countsOfAB()
method, the computation seems like:
Divide the 70 responses into 10 chunks of 7 responses each:
ABABABaABABABaABABABa...ABABABa <- Q -><- R -><- S ->...<- Z -> ABABABa: chunk Q ABABABa: chunk R ABABABa: chunk S ... ABABABa: chunk Z
From each chunk, the 1st response goes into its own grouping (i.e. the \0ドル^{th}\$ element of your
int[] countsA/B
array), the 2nd and 3rd responses goes into the next, and finally the last two into the 4th grouping:
$$ A \rbrace 0 \\ \left.\begin{matrix} B \\ A \\ \end{matrix}\right\rbrace 1 \\ \left.\begin{matrix} B \\ A \\ \end{matrix}\right\rbrace 2 \\ \left.\begin{matrix} B \\ a \\ \end{matrix}\right\rbrace 3 $$
- Collate all the groupings across the 10 chunks, so that for the example, we have the following representations:
$$\begin{array}{|c|c|c|} \hline Grouping & Count for A & Count for B\\ \hline 0 & 10 & 0 \\ \hline 1 & 10 & 10 \\ \hline 2 & 10 & 10 \\ \hline 3 & 10 & 10 \\ \hline \end{array} $$
With a bit of math, it is not hard to combine these into a single step, especially when you involve Java 8's stream:
private static final int GROUPING = 7;
private static Map<Choice, Map<Integer, Long>> compute(String input) {
return join(IntStream.range(0, input.length()).mapToObj(i -> map(input, i)))
.collect(Collectors.groupingBy(Entry::getKey,
Collectors.groupingBy(Entry::getValue, Collectors.counting())));
}
private static Map<Choice, Integer> map(String input, int i) {
return Collections.singletonMap(Choice.of(input.charAt(i)), ((i % GROUPING) + 1) / 2);
}
private static <K, V> Stream<Entry<K, V>> join(Stream<Map<K, V>> stream) {
return stream.map(Map::entrySet).flatMap(Set::stream);
}
join()
is a simple utility method that converts a Stream
of Map
elements into a Stream
of their Entry
contents.
The magic lies in the map(String, int)
method, which creates a Choice
\$\to\$ grouping
mapping by the formula ((i % GROUPING) + 1) / 2
(which is implicitly truncated to an int
):
$$\begin{array}{|c|c|} \hline i & f(i)\\ \hline 0 & 0 \\ \hline 1 & 1 \\ \hline 2 & 1 \\ \hline 3 & 2 \\ \hline 4 & 2 \\ \hline 5 & 3 \\ \hline 6 & 3 \\ \hline 7 & 0 \\ \hline 8 & 1 \\ \hline 9 & 1 \\ \hline \cdots & \cdots \\ \hline 67 & 2 \\ \hline 68 & 3 \\ \hline 69 & 3 \\ \hline \end{array} $$
In the compute()
method, we create these mappings per response, join()
them together, and then return the desired Map<Choice, Map<Integer, Long>>
object groupingBy()
the Choice
key (using Entry::getKey
as a method reference), and counting()
the values (Entry::getValue
) after groupingBy()
them as well.
The Map
representation for the example is thus:
{B={1=10, 2=10, 3=10}, A={0=10, 1=10, 2=10, 3=10}}
There is no 0=...
result for choice B
, since all the responses in that grouping are A
.
Calculating the scores and mapping
Next, we need to calculate the percentages of B responses per group, i.e. we will have 0% for group 0 and 50% for the rest for the example.
From the Map<Choice, Map<Integer, Long>>
result we have previously, it is again not hard to apply Java 8's stream processing again to perform the calculation and do the mapping.
Let's introduce some constants first:
private static final String[][] TRAITS = Stream.of("EXI", "SXN", "TXF", "JXP")
.map(v -> v.split(""))
.toArray(String[][]::new);
private static final Map<Integer, Long> BLANK =
Collections.unmodifiableMap(IntStream.range(0, TRAITS.length).boxed()
.collect(Collectors.toMap(i -> i, i -> 0L)));
private static final int THRESHOLD = 50;
TRAITS
is a similar take on the array-based lookup approach suggested in @Antot's answer, except that it is String[][]
-based and contains the "X"
values for easier lookups when the percentage is exactly 50%, i.e. THRESHOLD
. BLANK
represents a Map
where there are no responses for a given choice.
public static String getPersonality(String input) {
Map<Choice, Map<Integer, Long>> map = compute(Objects.requireNonNull(input));
return join(Stream.of(Choice.A, Choice.B).map(v -> map.getOrDefault(v, BLANK)))
.collect(Collectors.groupingBy(Entry::getKey,
Collectors.summingDouble(Entry::getValue)))
.entrySet()
.stream()
.map(entry -> derive(map.getOrDefault(Choice.B, BLANK), entry))
.collect(Collectors.joining());
}
private static String derive(Map<Integer, Long> m, Entry<Integer, Double> i) {
return TRAITS[i.getKey()][index(m.getOrDefault(i.getKey(), 0L), i.getValue())];
}
private static int index(long numerator, double denominator) {
int value = (int) Math.round(100 * numerator / denominator);
return value < THRESHOLD ? 0 : value == THRESHOLD ? 1 : 2;
}
Since we are only interested in A
and B
responses, we query for the map
results via Stream.of(Choice.A, Choice.B).map(v -> map.getOrDefault(v, BLANK))
. Using BLANK
as a default value here is crucial to provide 0
s for the calculations later.
At the end of the first collect()
step, what we have is a Map<Integer, Double>
representing the summation of responses per grouping, by applying groupingBy()
on the key and summingDouble()
on the values.
For each of the entries (groupings \0ドル\ldots3\$), we can now derive the personality trait with the method derive()
. Its first argument is the results of B
responses from the compute()
method, and the second argument is the Map
entry of grouping
\$\to\$ total responses of the grouping.
From this entry, the key (grouping) is used for the first dimension of our TRAITS
array, and the second dimension is the result of computing our 'B-percentage' in the index()
method.
Finally, each resulting String
from the derive()
method is joined together to give the four-letter personality by using Collectors.joining()
.
Conclusion
This is quite a lengthy answer, but I hope it adequately describes how the various steps in your solution can be re-imagined and re-composed into what are essentially two stream-based operations (helper methods aside).