Skip to content

Latest commit

 

History

History
254 lines (180 loc) · 12.7 KB

README.rst

File metadata and controls

254 lines (180 loc) · 12.7 KB

Fun and learning with the PlantUML preprocessor

The PlantUML preprocessor provides a higher level abstraction to generate the diagram descriptions. It can also be used as a programming language to compute the diagrams! The preprocessor provides variables, conditional expressions, looping constructs, and functions, among other facilities. The programs can also take external input through a file.

This also enables having some fun while learning the preprocessor features. In this document, I describe some such examples. These example diagrams are computed by PlantUML, not manually described.

All of them are my original work.

💡 TIP

Some diagrams below are too large to view easily on the webpage. You can open them in a new tab/window or download them for better clarity. The images are in SVG format, allowing you to zoom in for a clearer view.

This hacks a state diagram to denote the recursive calls and the computed factorial value for each number. The !function feature defines the factorial function that computes the factorial, creates the states and draws the connections between the states. The preprocessor not only computes but also draws the recursion!

@startuml
!procedure $create_state($name, $attribute)
state $name : $attribute
!endprocedure
!procedure $connect_states($name1, $name2)
$name1 -right-> $name2
!endprocedure
/' computes the factorial for all numbers from 0 to the specified number,
creates a state for each number, with its factorial as the attribute,
and connects every state to its preceding state, denoting the recursive
nature of the factorial function computation. '/
!function $factorial($n)
!assert ($n >= 0) : "input is " + $n + " but should be non-negative."
!if ($n == 0)
!$value = 1
$create_state($n, $value)
!else
!$value = $n * $factorial($n - 1)
$create_state($n, $value)
$connect_states($n, $n - 1)
!endif
!return $value
!endfunction
' assign the return value to avoid syntax error
!$ignore = $factorial(10)
@enduml

State Diagram hack showing factorial computation

This hacks the preprocessor's JSON parsing utility and !assert directive to run "unit tests" for the factorial function. For an invalid input, that is a negative number, the factorial function raises an assert. However, there is no facility to expect an assertion or exception (which is fair as the preprocessor is not a unit testing framework). The "failed" unit test appears as an assertion error in the generated diagram.

@startuml
!include factorial_demo.puml
!$test_data = {
"tests" : [
{"input": "0", "output": "1"},
{"input": "1", "output": "1"},
{"input": "2", "output": "2"},
{"input": "3", "output": "6"},
{"input": "4", "output": "24"},
{"input": "5", "output": "120"},
{"input": "6", "output": "720"},
{"input": "7", "output": "5040"},
{"input": "8", "output": "40320"},
{"input": "9", "output": "362880"},
{"input": "10", "output": "3628800"},
{"input": "11", "output": "39916800"},
{"input": "12", "output": "479001600"},
/' below input will raise an assert, but there's currently no way to
"expect" an assert, preprocessor is not a unit testing framework. ;-) '/
{"input": "-1", "output": "0"}
]
}
!foreach $item in $test_data.tests
!$expected = %intval($item.output)
!$actual = $factorial(%intval($item.input))
!assert $expected == $actual : "expected [" + $expected + "] actual [" + $actual + "]"
!endfor
@enduml

Hack showing unit testing of factorial function

The factorial function is now memoized to reuse the computed factorial value for a number. As the preprocessor does not provide any array or hashmap data structure, string concatenation is hacked to create a hashmap (dictionary).

@startuml
/' computes the factorial of the given integer. '/
!function $factorial($n)
/' Return value of this function is memoized because it uses recursion.
PlantUML preprocessor doesn't provide dictionary or array data structure,
hence a "hack" simulates a dictionary. For every input integer, a variable
is created with the stringized integer as its name, and the return value is
assigned to it. Thus, the variable's name serves as the "key". '/
!if %variable_exists(%string($n))
!return %get_variable_value(%string($n))
!endif
!if $n == 0
!$value = 1
!else
!$value = $n * $factorial($n - 1)
!endif
%set_variable_value(%string($n), $value)
!return $value
!endfunction
!procedure $factorial_question_answer_sequence(\
$starting_number = 0,\
$ending_number = 12,\
$color_number_in_question = blue,\
$color_number_in_answer = green)
skinparam SequenceMessageAlignment direction
!$number = $starting_number
!while $number <= $ending_number
Question -> Answer : What is factorial of <color:$color_number_in_question>**$number**</color>?
Question <- Answer : Factorial of <color:$color_number_in_question>**$number**</color> is <color:$color_number_in_answer>**$factorial($number)**</color>.
|||
!$number = $number + 1
!endwhile
!endprocedure
scale 1.5 /' zooms the generated diagram per specified scaling factor '/
$factorial_question_answer_sequence(\
$color_number_in_answer = darkviolet, $color_number_in_question = red\
)
@enduml

Compute factorial for a sequence of numbers

The state diagram is once again hacked to draw a fibonacci recursion tree. This diagram is more complicated to compute than the factorial recursion because:

  1. Each non-leaf node connects to two other nodes.
  2. There are multiple instances of leaf node and non-leaf nodes in the tree, so a mechanism is needed to identify each such instance.
  3. As there are multiple leaf nodes, the end of recursion needs to be computed carefully to prevent an accidental endless recursion.

I called this computation out-in because it primarily focusses on drawing the tree, while computing and filling in the fibonacci series numbers along the way.

I created this approach in my early days of learning when I was less well-versed with using the preprocessor for recursion. I created the more intuitive in-out approach shortly afterwards.

@startuml
!function $fibonacci($n)
!if $n == 0
!return 0
!elseif $n == 1
!return 1
!else
/' create named variables because it seems the preprocessor cannot
handle two unnamed temporary variables. Following code crashes at
"compile time":
!return $fibonacci($n - 1) + $fibonacci($n - 2) '/
!$fibonacci_n_minus_1 = $fibonacci($n - 1)
!$fibonacci_n_minus_2 = $fibonacci($n - 2)
!return $fibonacci_n_minus_1 + $fibonacci_n_minus_2
!endif
!endfunction
!procedure $create_state($name, $instance_name, $value)
state "$name" as $instance_name
$instance_name : $value
!endprocedure
!procedure $connect_states($instance1_name, $instance2_name)
$instance1_name -down-> $instance2_name
!endprocedure
/' draws the fibonacci recursion tree for the specified integer n. '/
!procedure $make_fibonacci_tree($n, $index = 0)
!if $n >= 0
!$fibonacci_n = $fibonacci($n)
$create_state($n, $index, $fibonacci_n)
/' as the same number can appear multiple times in the fibonacci tree,
$index is used to create distinct node instances. the procedure is
called recursively choosing a different $index value for each call.
heap numbering approach is used to compute the $index value. '/
!endif
!if $n >= 2
!$left_child_instance_name = $index * 2 + 1
$make_fibonacci_tree($n - 1, $left_child_instance_name)
$connect_states($index, $left_child_instance_name)
!$right_child_instance_name = $index * 2 + 2
$make_fibonacci_tree($n - 2, $right_child_instance_name)
$connect_states($index, $right_child_instance_name)
!endif
!endprocedure
$make_fibonacci_tree(10)
@enduml

Fibonacci recursion tree using out-in computation

This also hacks the state diagram to draw the fibonacci recursion tree. However, it uses an in-out computation, where the primary focus is on computing the fibonacci series numbers, then drawing the nodes (states) around them and connecting them to form the tree.

This code is more intuitive for a human reader as it looks similar to the naive recursive Fibonacci implementation in conventional programming languages.

@startuml
!procedure $create_state($name, $instance_name, $value)
/' just to demonstrate customization based on value, nothing special about this check. '/
!if $name >= 4
!$color = "#LightSalmon"
!else
!$color = ""
!endif
state "$name" as $instance_name $color
$instance_name : $value
!endprocedure
!procedure $connect_states($instance1_name, $instance2_name)
$instance1_name -down-> $instance2_name
!endprocedure
!function $fibonacci($n, $index = 0)
!if $n == 0
!$fibonacci_n = 0
!elseif $n == 1
!$fibonacci_n = 1
!else
/' preprocessor seems unable to maintain two unnamed temporary variables,
hence create local variables to hold return values of the recursive calls. '/
!$fibonacci_n_minus_1 = $fibonacci($n - 1, $index * 2 + 1)
!$fibonacci_n_minus_2 = $fibonacci($n - 2, $index * 2 + 2)
!$fibonacci_n = $fibonacci_n_minus_1 + $fibonacci_n_minus_2
!endif
$create_state($n, $index, $fibonacci_n)
!if $n >= 2
$connect_states($index, $index * 2 + 1)
$connect_states($index, $index * 2 + 2)
!endif
!return $fibonacci_n
!endfunction
!$ignore = $fibonacci(10)
@enduml

Fibonacci recursion tree using in-out computation

The fibonacci computation is memoized using a hack similar to the one used for the factorial function, and the drawing illustrates the memoized computation. The drawings of the recursive and the memoized approach can be a useful visualization to demonstrate how memoization cuts off the recursive calls.

Memoized fibonacci computation

This was an intermediate diagram computation I created while trying to solve the fibonacci recursion tree diagram computation described above.

@startuml
!procedure $connect_entities($name1, $name2)
$name1 -down-> $name2
!endprocedure
/' draws the full binary tree of the specified height. '/
!procedure $make_full_binary_tree($height, $index = 0)
!if $height >= 0
object $index
!endif
!if $height > 0
!$left_child_instance_name = $index * 2 + 1
$make_full_binary_tree($height - 1, $left_child_instance_name)
$connect_entities($index, $left_child_instance_name)
!$right_child_instance_name = $index * 2 + 2
$make_full_binary_tree($height - 1, $right_child_instance_name)
$connect_entities($index, $right_child_instance_name)
!endif
!endprocedure
$make_full_binary_tree(5)
@enduml

Binary tree

PlantUML preprocessor has first class functions and even lambda functions! This is unexpectedly remarkable for what was not even intended to be a programming language.

I customized the node (state) and the value formatting by passing in a user-defined function and a lambda expression. These are respectively used to draw the leaf nodes with a different background colour, and a different border style for nodes having values up to 10.

@startuml
!function $is_leaf_node($state_name)
!return $state_name == 0 || $state_name == 1
!endfunction
!function $always_false($ignore)
!return %false()
!endfunction
/' creates a state with options to highlight it in two ways,
based on a user-defined function and a lambda function respectively.'/
!procedure $create_state(\
$name,\
$instance_name,\
$value,\
$name_based_highlight_function = $always_false,\
$value_based_highlight_expression = %false())
!if %call_user_func($name_based_highlight_function, $name)
!$background_color = "#technology"
!else
!$background_color = ""
!endif
!if %eval($value_based_highlight_expression)
!$border_style = "##[bold]blue"
!else
!$border_style = ""
!endif
state "$name" as $instance_name $background_color $border_style
$instance_name : $value
!endprocedure
!procedure $connect_states($instance1_name, $instance2_name)
$instance1_name -down-> $instance2_name
!endprocedure
!function $fibonacci($n, $index = 0)
!if $n == 0
!$fibonacci_n = 0
!elseif $n == 1
!$fibonacci_n = 1
!else
!$fibonacci_n_minus_1 = $fibonacci($n - 1, $index * 2 + 1)
!$fibonacci_n_minus_2 = $fibonacci($n - 2, $index * 2 + 2)
!$fibonacci_n = $fibonacci_n_minus_1 + $fibonacci_n_minus_2
!endif
$create_state($n, $index, $fibonacci_n, "$is_leaf_node", "$value <= 10")
!if $n >= 2
$connect_states($index, $index * 2 + 1)
$connect_states($index, $index * 2 + 2)
!endif
!return $fibonacci_n
!endfunction
!$ignore = $fibonacci(10)
@enduml

User-defined function and lambda function

This hacks the Work Breakdown Structure (WBS) diagram to represent Test cricket matches hosting data in a hierarchical structure. The JSON data file contains the following information:

  1. Number of matches played at various cricket grounds.
  2. Hierarchical structure of the location (city, country) the cricket ground belongs to.

The WBS diagram represents the hierarchy as a tree to any depth, while also adding up the count at every level beneath it. This naturally resembles the Composite design pattern, and the computation follows such a recursive structure.

In addition, the grounds that have hosted 100 or more matches are highlighted.

@startwbs
!input_json_file ?= "src/preprocessor/cricket_grounds_test_matches_hosted.json"
!$test_cricket_matches_data = %load_json(input_json_file)
!function $repeat_string($input_string, $times)
!$str = ""
!while $times != 0
!$str = $str + $input_string
!$times = $times - 1
!endwhile
!return $str
!endfunction
!function $make_wbs_code($name, $value, $level, $highlight_expression = %false())
!$background_colour = ""
!if %eval($highlight_expression)
!$background_colour = "[#PaleGreen]"
!endif
!$wbs_code \
= $repeat_string("*", $level) + $background_colour \
+ " <b>" + $name + "</b> " + $value + %newline()
!return $wbs_code
!endfunction
!function $generate_wbs_code($data, $level = 1)
!$code_string = ""
!$sum = 0
!foreach $item in $data.list
!$inner_item_wbs_code = ""
!$inner_item_name = ""
!if %strlen($item.list) != 0 /' hack to check if it is a leaf node '/
/' bug/hack: need to store the inner item's name before recursing. below we
update a dictionary where each item is mapped to the sum of the counts below
it. the preprocessor seems to have either an implementation bug or stack
limitation, so $item.name gets replaced at each level, thus the sum gets
mapped to the innermost item's name instead.'/
!$inner_item_name = $item.name
/' it is not a leaf node, recurse to the inner level. '/
!$inner_item_wbs_code = $generate_wbs_code($item, $level + 1)
/' recursive function call at the inner level would have mapped the sum in
the dictionary, so add it to the running sum at this level. '/
!$sum = $sum + %get_variable_value($inner_item_name)
!else
/' it is a leaf node, so it is placed at the next level in the wbs code. '/
!$count = %intval($item.count)
!$inner_item_wbs_code \
= $make_wbs_code($item.name, $count, $level + 1, "$value >= 100")
/' for a leaf node, the sum is simply its own count. '/
!$sum = $sum + $count
!endif
/' concatenate the wbs code of this item to the overall wbs code for this level.
this works correctly for leaf nodes or non-leaf nodes or mix of the two. leaf
node's wbs code is directly computed here. for non-leaf nodes, recursive
function call computes the wbs code for lower levels, then that level's wbs code
is pre-concatenated outside the loop and returned as the overall wbs code for
that level. '/
!$code_string = $code_string + $inner_item_wbs_code
!endfor
/' pre-concatenate this level's wbs code to the lower level's wbs code '/
!$code_string = $make_wbs_code($data.name, $sum, $level) + $code_string
/' map the sum of the counts below this level to this level's name. during the
recursion unwinding, the outer level functions can refer to this dictionary entry
to update the count at that level. '/
%set_variable_value($data.name, $sum)
!return $code_string
!endfunction
<style>
wbsDiagram {
LineColor Black
arrow {
LineColor Blue
}
}
</style>
title <size:40>**Test cricket matches hosted by cricket grounds**</size>
legend top center
<size:24>Last updated: %date()</size>
|<#PaleGreen> | Hosted 100 or more matches |
end legend
/' generates wbs (work breakdown structure) diagram from json data.
the json data has a recursive structure, consisting of the following fields:
- name: string
- count: integer (OR)
- list: array of this data structure recursively '/
$generate_wbs_code($test_cricket_matches_data)
@endwbs

{
"name": "All", "list": [
{"name": "India", "list": [
{"name": "Eden Gardens, Kolkata", "count": 42},
{"name": "Chinnaswamy Stadium, Bengaluru", "count": 24},
{"name": "Mumbai", "list": [
{"name": "Wankhede Stadium", "count": 26},
{"name": "Brabourne Stadium", "count": 18},
{"name": "Gymkhana Ground", "count": 1}
]},
{"name": "Green Park, Kanpur", "count": 23},
{"name": "Arun Jaitley Stadium, Delhi", "count": 35},
{"name": "Chennai", "list": [
{"name": "Chidambaram Stadium", "count": 34},
{"name": "Nehru Stadium", "count": 9}
]},
{"name": "Narendra Modi Stadium, Ahmedabad", "count": 15},
{"name": "Chandigarh", "list": [
{"name": "IS Bindra Stadium", "count": 14},
{"name": "Sector 16 Stadium", "count": 1}
]},
{"name": "Nagpur", "list": [
{"name": "VCA Ground", "count": 9},
{"name": "VCA Stadium", "count": 7}
]},
{"name": "Hyderabad", "list": [
{"name": "Rajiv Gandhi International Stadium", "count": 6},
{"name": "Lal Bahadur Shastri Stadium", "count": 3}
]},
{"name": "Barabati Stadium, Cuttack", "count": 2},
{"name": "Dehradun Cricket Stadium, Dehradun", "count": 1},
{"name": "HPCA Stadium, Dharamsala", "count": 2},
{"name": "Holkar Cricket Stadium, Indore", "count": 3},
{"name": "Sawai Mansingh Stadium, Jaipur", "count": 1},
{"name": "Gandhi Stadium, Jalandhar", "count": 1},
{"name": "Khanderi Cricket Stadium, Rajkot", "count": 3},
{"name": "MCA Stadium, Pune", "count": 2},
{"name": "JSCA International Stadium, Ranchi", "count": 3},
{"name": "ACA-VDCA Stadium, Visakhapatnam", "count": 3},
{"name": "Lucknow", "list": [
{"name": "Ekana Cricket Stadium", "count": 1},
{"name": "KD Singh Stadium", "count": 1},
{"name": "University Ground", "count": 1}
]}
]},
{"name": "Australia", "list": [
{"name": "Adelaide Oval, Adelaide", "count": 82},
{"name": "Brisbane", "list": [
{"name": "Gabba", "count": 66},
{"name": "Exhibition Ground", "count": 2}
]},
{"name": "MCG, Melbourne", "count": 116},
{"name": "Perth", "list": [
{"name": "WACA", "count": 44},
{"name": "Perth Stadium", "count": 4}
]},
{"name": "SCG, Sydney", "count": 112},
{"name": "Bellerive Oval, Hobart", "count": 14},
{"name": "Cazaly's Stadium, Cairns", "count": 2},
{"name": "Manuka Oval, Canberra", "count": 1},
{"name": "Marrara Cricket Ground, Darwin", "count": 2}
]},
{"name": "Pakistan", "list": [
{"name": "Lahore", "list": [
{"name": "Gaddafi Stadium", "count": 41},
{"name": "Bagh-e-Jinnah", "count": 3}
]},
{"name": "National Stadium, Karachi", "count": 43},
{"name": "Karachi", "list": [
{"name": "National Stadium", "count": 47},
{"name": "Defence Housing Authority Stadium", "count": 1}
]},
{"name": "Iqbal Stadium, Faisalabad", "count": 24},
{"name": "Rawalpindi", "list": [
{"name": "Rawalpindi Cricket Stadium", "count": 13},
{"name": "Pindi Club Ground", "count": 1}
]},
{"name": "Peshawar", "list": [
{"name": "Arbab Niaz Stadium", "count": 6},
{"name": "Peshawar Club Ground", "count": 1}
]},
{"name": "Multan", "list": [
{"name": "Multan Cricket Stadium", "count": 6},
{"name": "Ibn-e-Qasim Bagh Stadium", "count": 1}
]},
{"name": "Niaz Stadium, Hyderabad", "count": 5},
{"name": "Jinnah Stadium, Sialkot", "count": 4},
{"name": "Bahawal Stadium, Bahawalpur", "count": 1},
{"name": "Jinnah Stadium, Gujranwala", "count": 1},
{"name": "Sheikhupura Stadium, Sheikhupura", "count": 2}
]},
{"name": "England", "list": [
{"name": "London", "list": [
{"name": "Kennington Oval", "count": 106},
{"name": "Lord's", "count": 146}
]},
{"name": "Trent Bridge, Nottingham", "count": 66},
{"name": "Edgbaston, Birmingham", "count": 55},
{"name": "Headingley, Leeds", "count": 81},
{"name": "Old Trafford, Manchester", "count": 84},
{"name": "Riverside Ground, Chester-Le-Street", "count": 6},
{"name": "Bramall Lane, Sheffield", "count": 1},
{"name": "The Rose Bowl, Southampton", "count": 7},
{"name": "Sophia Gardens, Cardiff", "count": 3}
]},
{"name": "South Africa", "list": [
{"name": "Supersport Park, Centurion", "count": 29},
{"name": "Newlands, Cape Town", "count": 60},
{"name": "Johannesburg", "list": [
{"name": "Wanderers Stadium", "count": 44},
{"name": "Old Wanderers", "count": 22},
{"name": "Ellis Park", "count": 6}
]},
{"name": "Durban", "list": [
{"name": "Kingsmead", "count": 45},
{"name": "Lord's", "count": 4}
]},
{"name": "St George's Park, Port Elizabeth", "count": 32},
{"name": "Mangaung Oval, Bloemfontein", "count": 5},
{"name": "Buffalo Park, East London", "count": 1},
{"name": "Senwes Park, Potchefstroom", "count": 2}
]},
{"name": "New Zealand", "list": [
{"name": "Christchurch", "list": [
{"name": "AMI Stadium", "count": 40},
{"name": "Hagley Oval", "count": 13}
]},
{"name": "Basin Reserve, Wellington", "count": 68},
{"name": "Eden Park, Auckland", "count": 50},
{"name": "Seddon Park, Hamilton", "count": 28},
{"name": "Dunedin", "list": [
{"name": "Carisbrook", "count": 10},
{"name": "University Oval", "count": 8}
]},
{"name": "Bay Oval, Mount Maunganui", "count": 5},
{"name": "McLean Park, Napier", "count": 10}
]},
{"name": "Sri Lanka", "list": [
{"name": "Colombo", "list": [
{"name": "Sinhalese Sports Club Ground", "count": 45},
{"name": "P Sara Oval", "count": 22},
{"name": "R Premadasa Stadium", "count": 9},
{"name": "Colombo Cricket Club Ground", "count": 3}
]},
{"name": "Galle Stadium, Galle", "count": 44},
{"name": "Kandy", "list": [
{"name": "Asgiriya Stadium", "count": 21},
{"name": "Pallekele Cricket Stadium", "count": 9}
]},
{"name": "Tyronne Fernando Stadium, Moratuwa", "count": 4}
]},
{"name": "West Indies", "list": [
{"name": "Trinidad & Tobago", "list": [
{"name": "Queen's Park Oval, Port of Spain", "count": 62}
]},
{"name": "Barbados", "list": [
{"name": "Kensington Oval, Bridgetown", "count": 55}
]},
{"name": "Jamaica", "list": [
{"name": "Sabina Park, Kingston", "count": 54}
]},
{"name": "Guyana", "list": [
{"name": "Bourda, Georgetown", "count": 30},
{"name": "Providence Stadium, Providence", "count": 2}
]},
{"name": "Antigua & Barbuda", "list": [
{"name": "Antigua Recreation Ground, St. John's", "count": 22},
{"name": "Sir Vivian Richards Stadium, North Sound", "count": 12}
]},
{"name": "St Lucia", "list": [
{"name": "Daren Sammy Stadium, Gros Islet", "count": 10}
]},
{"name": "St Kitts & Nevis", "list": [
{"name": "Warner Park, Basseterre", "count": 3}
]},
{"name": "Dominica", "list": [
{"name": "Windsor Park, Roseau", "count": 6}
]},
{"name": "St Vincent", "list": [
{"name": "Arnos Vale Ground, Kingstown", "count": 3}
]},
{"name": "Grenada", "list": [
{"name": "National Cricket Stadium, St. George's", "count": 4}
]}
]},
{"name": "Bangladesh", "list": [
{"name": "Shaheed Chandu Stadium, Bogra", "count": 1},
{"name": "Chittagong", "list": [
{"name": "MA Aziz Stadium", "count": 8},
{"name": "Zahur Ahmed Chowdhury Stadium", "count": 24}
]},
{"name": "Dhaka", "list": [
{"name": "Bangabandhu National Stadium", "count": 17},
{"name": "Shere Bangla National Stadium", "count": 27}
]},
{"name": "Khan Shaheb Osman Ali Stadium, Fatullah", "count": 2},
{"name": "Sheikh Abu Naser Stadium, Khulna", "count": 3},
{"name": "Sylhet Stadium, Sylhet", "count": 3}
]},
{"name": "United Arab Emirates", "list": [
{"name": "Abu Dhabi", "list": [
{"name": "Sheikh Zayed Stadium", "count": 15},
{"name": "Tolerance Oval", "count": 1}
]},
{"name": "Dubai Cricket Stadium, Dubai", "count": 13},
{"name": "Sharjah Cricket Stadium, Sharjah", "count": 9}
]},
{"name": "Zimbabwe", "list": [
{"name": "Bulawayo", "list": [
{"name": "Queens Sports Club", "count": 25},
{"name": "Bulawayo Athletic Club", "count": 1}
]},
{"name": "Harare Sports Club, Harare", "count": 39}
]},
{"name": "Ireland", "list": [
{"name": "The Village, Dublin", "count": 1}
]}
]}

Hierarchical structure representing Test matches hosting data

As a further enhancement, the diagram can be updated semi-automatically. Using Python, I scrape the howstat.com data source , clean and transform the data into a JSON file, which is then used to generate the diagram. With Github Actions, this process is automated and the updated diagram is committed to the repository.

The Github Actions workflow generate-test-cricket-matches-hosted-diagram uses the Python project helicopter and the reusable workflow fly-helicopter from my other repository dragondive/masadora along with my composite action run-plantuml-action.

Hierarchical structure representing Test matches hosting data, auto-generated from scraped data

This diagram computation draws the Collatz sequence using rectangle objects. Multiple separate diagrams are drawn for the positive integers up to 100.

At the time of this writing, the preprocessor does not support modulo division, so I used my primary school (class 1) definition of even number (numbers ending in digits 0, 2, 4, 6, 8) 😸. I also used this hack to wrap the chain like a snake 🐍 for both readability and aesthetics. This overrides the default PlantUML behaviour of connecting the rectangles in a straight line, which can be tiring to read for some long chains.

The Collatz sequence chains are shown only for a few selected numbers below. The complete set of chains is available in the directory diagrams.

@startuml
!procedure $connect_entities($name1, $name2, $direction)
$name1 -$direction-> $name2
!endprocedure
!procedure $create_rectangle($name, $stereotype)
rectangle $name <<$stereotype>>
!endprocedure
!function $is_even($num)
/' as the preprocessor currently doesn't have a modulus operator, use this
"hack" of checking the last digit to decide if the number is even.
the "mathematical approach" of successive subtraction to determine the
remainder runs into memory problems at runtime for long sequences. '/
!$num_str = %string($num)
!$last_digit = %substr($num_str, %strlen($num_str) -1, 1)
!return ($last_digit == "0"\
|| $last_digit == "2"\
|| $last_digit == "4"\
|| $last_digit == "6"\
|| $last_digit == "8")
!endfunction
' unit tests for the $is_even function :-)
!assert $is_even(0) == 1
!assert $is_even(11) == 0
!assert $is_even(292) == 1
!assert $is_even(1663) == 0
!assert $is_even(10824) == 1
!assert $is_even(652665) == 0
!assert $is_even(9188316) == 1
!assert $is_even(79430237) == 0
!assert $is_even(340931238) == 1
!assert $is_even(394237819) == 0
!assert $is_even(518026430) == 1
!function $get_last_hex_digit($num)
!$num_str = %dec2hex($num)
!$last_digit = %substr($num_str, %strlen($num_str) -1, 1)
!return $last_digit
!endfunction
' unit tests for the $get_last_hex_digit function :-)
!assert $get_last_hex_digit(0) == "0"
!assert $get_last_hex_digit(17) == "1"
!assert $get_last_hex_digit(290) == "2"
!assert $get_last_hex_digit(1667) == "3"
!assert $get_last_hex_digit(10820) == "4"
!assert $get_last_hex_digit(652661) == "5"
!assert $get_last_hex_digit(9188310) == "6"
!assert $get_last_hex_digit(79430247) == "7"
!assert $get_last_hex_digit(340931240) == "8"
!assert $get_last_hex_digit(394237817) == "9"
!assert $get_last_hex_digit(518026426) == "a"
!assert $get_last_hex_digit(390243419) == "b"
!assert $get_last_hex_digit(890540140) == "c"
!assert $get_last_hex_digit(623489357) == "d"
!assert $get_last_hex_digit(216487342) == "e"
!assert $get_last_hex_digit(938476831) == "f"
!procedure $make_direction_map()
/' use this workaround to manually wrap the chain like a snake. :-)
otherwise the chain will grow in only one direction, which would be inefficient
use of the defined "canvas" and also difficult to read. as the preprocessor currently
does not have a modulus operator, use this workaround to map a direction manually. '/
%set_variable_value("0", "right")
%set_variable_value("1", "right")
%set_variable_value("2", "right")
%set_variable_value("3", "right")
%set_variable_value("4", "right")
%set_variable_value("5", "right")
%set_variable_value("6", "right")
%set_variable_value("7", "down")
%set_variable_value("8", "left")
%set_variable_value("9", "left")
%set_variable_value("a", "left")
%set_variable_value("b", "left")
%set_variable_value("c", "left")
%set_variable_value("d", "left")
%set_variable_value("e", "left")
%set_variable_value("f", "down")
!endprocedure
$make_direction_map()
!function $collatz($in_num)
!if $is_even($in_num)
!return $in_num / 2
!else
!return 3 * $in_num + 1
!endif
!endfunction
!assert $collatz(13) == 40
!assert $collatz(40) == 20
!procedure $make_collatz_sequence($num)
!if $num > 0
!$counter = 0
$create_rectangle($num, $counter)
!while $num != 1
!$prev = $num
!$num = $collatz($num)
!$direction = %get_variable_value($get_last_hex_digit($counter))
!$counter = $counter + 1
$create_rectangle($num, $counter)
$connect_entities($prev, $num, $direction)
!endwhile
!else
!log input is $num but should be positive.
!endif
!endprocedure
!$count = 0
!$limit = 100
!while $count <= $limit
title Collatz sequence for $count
$make_collatz_sequence(%intval($count))
!$count = $count + 1
newpage
!endwhile
@enduml

Collatz sequence for 9 Collatz sequence for 43 Collatz sequence for 97

A diagram template defines the overall structure with some customizable portions. A JSON file is read to configure those customizable portions to generate multiple diagram instances.

The below demo shows three diagrams being generated for each customer. The customer data specified in JSON format is used to customize the diagrams. Both values and objects appearing in the diagram can be customized.

‼️ MISSING FEATURE/BUG

In the sequence diagrams, the actors not involved on the current page also appear, which is usually not the desired behaviour. This is due to a missing feature/bug in PlantUML reported on the PlantUML forum here.

@startuml
!$customer_data = {
"customer_list" : [
{
"name": "Initech",
"is_premium": "false",
"delivery_path": "https://initech.com/tps/",
"release_number": "VIII.XV.XXXIX" /' If you could use Roman numerals for the release numbers, that would be great.'/
},
{
"name": "Acme",
"is_premium": "true",
"delivery_path": "https://acme.com/creativemayhem/",
"release_number": "4.1.6"
}
]
}
actor Developer
actor Tester
actor Integrator
!foreach $customer in $customer_data.customer_list
!assert $customer.name != ""
!assert $customer.delivery_path != ""
!assert $customer.release_number != ""
newpage Customer $customer.name: Developer Workflow
Developer -> %string("dev_" + $customer.name) : publish code changes
!if ($customer.is_premium == "true")
alt if premium feature
Developer -> Developer : prepare Premium Order Report
Developer -> %string("dev_" + $customer.name) : publish premium feature
end
!endif
newpage Customer $customer.name: Tester Workflow
Tester <- %string("dev_" + $customer.name) : checkout %string("RC_" + $customer.release_number)
Tester <- Tester : run all tests\nand verify results
newpage Customer $customer.name: Integrator Workflow
Integrator -> Integrator : integrate all components\nfor project $customer.name
Integrator -> Integrator : publish libmagic.so to\n%string($customer.delivery_path + $customer.release_number)
!if ($customer.is_premium == "true")
alt if premium feature
Integrator -> Integrator : publish libpremium.so to\n%string($customer.delivery_path + $customer.release_number)
end
!endif
!endfor
@enduml

Diagrams generated for non-premium customer Initech

Developer Workflow for non-premium customer Initech Tester Workflow for non-premium customer Initech Integrator Workflow for non-premium customer Initech

Diagrams generated for premium customer Acme

Developer Workflow for premium customer Acme Tester Workflow for premium customer Acme Integrator Workflow for premium customer Acme