Rebol3 Code Examplex


Box the compass

Generate compass point names from degree values.

Rebol [
    title: "Rosetta code: Box the compass"
    file:  %Box_the_compass.r3
    url:   https://rosettacode.org/wiki/Box_the_compass
    needs: 3.10.0 ;; or something like that
]

form-angle: func[
    "Format the angle as a string with at least one decimal and pad to 2 decimals"
    angle [decimal!]
][
    angle: form round/to angle 0.01
    pad/with find/last angle #"." 3 #"0"  ;; ensure two digits after the decimal point
    append angle #"°"                     ;; add the degree symbol
]

compass: context [
    points: none

    make-points: function [/verbose][
        out: copy []
        ;; Define a character set for the four main directions
        main-dir: charset "NESW"
        ;; 32-wind compass notation (with quarter winds and by-points)
        words: [
            N NbE NNE NEbN NE NEbE ENE EbN E EbS ESE SEbE SE SEbS SSE
            SbE S SbW SSW SWbS SW SWbW WSW WbS W WbN WNW NWbW NW NWbN NNW NbW N
        ]
        ;; Replacement map from shorthand characters to full words (and spacing)
        replacement: #[
            #"N" "north"
            #"b" " by "
            #"S" "south"
            #"E" "east"
            #"W" "west"
        ]
        i: 0  ;; index of the current compass point (0..32)
        foreach word words [
            ;; Compute heading angle:
            ;; Each step is 11.25°, with offsets of +5.62, 0.0, or -5.62 for the 1/3 sub-steps.
            heading: i * 11.25 + (pickz [0.0 5.62 -5.62] i % 3)
            ;; Start with the symbolic label (e.g., "NEbE") as a mutable string
            text: to string! word
            ;; Insert a hyphen after the first main direction if followed by two more main directions
            ;; (e.g., "NEbE" -> "N-EbE")
            parse text [1 main-dir ahead 2 main-dir insert "-"]
            ;; Expand shorthand letters to full words using the replacement table:
            ;; - Replace N/S/E/W with north/south/east/west
            ;; - Replace 'b' with " by "
            parse text [any [p: change skip (any [replacement/(p/1) p/1])]]
            ;; Capitalize first letter of the expanded label (e.g., "north by east" -> "North by east")
            uppercase/part text 1
            if verbose [
                ;; Print three columns:
                ;; - Ordinal index within 32-wind cycle (1..32)
                ;; - Formatted angle
                ;; - Expanded textual compass point
                print [
                    pad (i % 32 + 1) -3
                    pad form-angle heading -7
                    pad form word 5
                    text
                ]
            ]
            repend out [heading word text]
            ++ i
        ]
        new-line/skip out true 3
        out
    ]

    nearest-text: function [
        "Returns the nearest compass point text for a given angle."
        angle     [number! integer! decimal!]
    ][
        ;; :lazily build the points table if not already built
        unless points [points: make-points]
        ;; Normalize query angle into [0,360)
        angle: angle % 360.0
        if angle < 0 [angle: angle + 360.0]

        best-diff: 1e9
        ;; Walk triples: angle word text
        foreach [deg wrd txt] points [
            ;; Compute minimal circular difference
            diff: abs deg - angle
            if diff < best-diff [
                best-diff: diff
                best: txt ;; remember the text for the closest heading
            ]
        ]
        best
    ]
]

;; Print all angle texts
compass/make-points/verbose

print "^/Resolve texts for random angles:"
random/seed 1
loop 5 [
    angle: random 360.0
    print [
        pad form-angle angle -7
        compass/nearest-text angle
    ]
]