nx_writecraft.nx source
↩ module page · 190 lines · 6833 B
1// nx_writecraft.nx -- WRITING arc, rung W-R0: the bedrock prose-measurement organ.
2// Every writing domain the operator named (fiction, non-fiction, business,
3// academic, research papers) rests on the same three counts -- words, sentences,
4// syllables -- and the readability grades derived from them. This is the
5// foundation the rest of nishi-write (outliner, style/grammar, citations,
6// export) builds on; it is also the first DIRECTLY exceed-able rung vs the
7// readability engines in Hemingway Editor / Grammarly / ProWritingAid / MS Word.
8//
9// Counting (heuristics, the same class the incumbents use):
10// - WORD = maximal run of ASCII letters (apostrophe kept inside a word:
11// "don't" = 1 word, 4 letters).
12// - SENTENCE = one run of terminal punctuation [.!?] (so "..." counts once).
13// - SYLLABLE = vowel-group count (a e i o u y), minus a silent trailing 'e',
14// EXCEPT a "consonant + le" ending keeps its syllable
15// ("table"=2, "make"=1); floor of 1.
16// - COMPLEX = a word of >=3 syllables (Gunning Fog / SMOG input).
17//
18// Readability is returned in MILLI-UNITS (value x 1000, integer, may be negative)
19// so the whole arc stays integer-only (no floats). The fixed-point formula is
20// exact where the divisions are exact and tracks the published real-number
21// formula elsewhere; the gate KATs both the exact case and the discrimination.
22// FRE (Flesch Reading Ease) x1000 = 206835 - 1015*W/S - 84600*Y/W
23// FKGL (Flesch-Kincaid Grade) x1000 = 390*W/S + 11800*Y/W - 15590
24// FOG (Gunning Fog index) x1000 = 400*W/S + 40000*C/W
25// where W=words S=sentences Y=syllables C=complex words.
26//
27// Pure integer, NO floats, NO syscalls, NO imports (caller owns all buffers) --
28// the nx_note shape, so any lane (tutor, local-LLM, daemon) can call it.
29//
30// wc_syllables(t, a, b) -- syllables in the word t[a..b) (b exclusive)
31// wc_counts(t, n, out) -- out[0]=letters out[1]=words out[2]=sentences
32// out[3]=syllables out[4]=complex; returns words
33// wc_flesch_reading_ease(out) -- FRE x1000 (higher = easier)
34// wc_fk_grade(out) -- FKGL x1000 (US grade level)
35// wc_gunning_fog(out) -- FOG x1000 (US grade level)
36//
37// license_tier: ORIGINAL
38// module: nishi-core.write.craft
39// depends:
40// capability: WRITE_PROSE_METRICS
41
42// lowercase an ASCII byte value held in an i64
43func wc_lower(c: i64) -> i64 {
44 if c >= 65 {
45 if c <= 90 { return c + 32 }
46 }
47 return c
48}
49
50// 1 if c is an ASCII letter, else 0
51func wc_is_alpha(c: i64) -> i64 {
52 let l: i64 = wc_lower(c)
53 if l >= 97 {
54 if l <= 122 { return 1 }
55 }
56 return 0
57}
58
59// 1 if c is a vowel (a e i o u y, case-insensitive), else 0
60func wc_is_vowel(c: i64) -> i64 {
61 let l: i64 = wc_lower(c)
62 if l == 97 { return 1 } // a
63 if l == 101 { return 1 } // e
64 if l == 105 { return 1 } // i
65 if l == 111 { return 1 } // o
66 if l == 117 { return 1 } // u
67 if l == 121 { return 1 } // y
68 return 0
69}
70
71// syllables in the word occupying t[a..b) (b exclusive)
72func wc_syllables(t: *u8, a: i64, b: i64) -> i64 {
73 var cnt: i64 = 0
74 var prev_v: i64 = 0
75 var i: i64 = a
76 while i < b {
77 let v: i64 = wc_is_vowel(t[i] as i64)
78 if v == 1 {
79 if prev_v == 0 { cnt = cnt + 1 }
80 }
81 prev_v = v
82 i = i + 1
83 }
84 // silent trailing 'e', with the "consonant + le" exception
85 if b - a >= 1 {
86 let last: i64 = wc_lower(t[b - 1] as i64)
87 if last == 101 { // ends in 'e'
88 var keep: i64 = 0
89 if b - a >= 3 {
90 let l2: i64 = wc_lower(t[b - 2] as i64) // char before final e
91 if l2 == 108 { // 'l' -> "...le"
92 let l3: i64 = t[b - 3] as i64
93 if wc_is_vowel(l3) == 0 { keep = 1 } // consonant + le
94 }
95 }
96 if keep == 0 {
97 if cnt > 1 { cnt = cnt - 1 }
98 }
99 }
100 }
101 if cnt == 0 { cnt = 1 }
102 return cnt
103}
104
105// fill out[0..4]; returns word count
106func wc_counts(t: *u8, n: i64, out: *i64) -> i64 {
107 var letters: i64 = 0
108 var words: i64 = 0
109 var sentences: i64 = 0
110 var syll: i64 = 0
111 var complex: i64 = 0
112 var wstart: i64 = 0 - 1 // -1 = not currently inside a word
113 var prev_term: i64 = 0
114 var i: i64 = 0
115 while i < n {
116 let c: i64 = t[i] as i64
117 var inword: i64 = 0
118 if wc_is_alpha(c) == 1 {
119 inword = 1
120 } else {
121 if c == 39 { // apostrophe stays in an open word
122 if wstart >= 0 { inword = 1 }
123 }
124 }
125 if inword == 1 {
126 if wstart < 0 { wstart = i }
127 if wc_is_alpha(c) == 1 { letters = letters + 1 }
128 } else {
129 if wstart >= 0 { // a word just ended at i
130 words = words + 1
131 let s: i64 = wc_syllables(t, wstart, i)
132 syll = syll + s
133 if s >= 3 { complex = complex + 1 }
134 wstart = 0 - 1
135 }
136 }
137 // sentence terminators: count each run once
138 var term: i64 = 0
139 if c == 46 { term = 1 } // .
140 if c == 33 { term = 1 } // !
141 if c == 63 { term = 1 } // ?
142 if term == 1 {
143 if prev_term == 0 { sentences = sentences + 1 }
144 }
145 prev_term = term
146 i = i + 1
147 }
148 if wstart >= 0 { // trailing word with no punctuation
149 words = words + 1
150 let s2: i64 = wc_syllables(t, wstart, n)
151 syll = syll + s2
152 if s2 >= 3 { complex = complex + 1 }
153 }
154 out[0] = letters
155 out[1] = words
156 out[2] = sentences
157 out[3] = syll
158 out[4] = complex
159 return words
160}
161
162// FRE x1000 = 206835 - 1015*W/S - 84600*Y/W (higher = easier to read)
163func wc_flesch_reading_ease(out: *i64) -> i64 {
164 var w: i64 = out[1]
165 var s: i64 = out[2]
166 let y: i64 = out[3]
167 if w < 1 { w = 1 }
168 if s < 1 { s = 1 }
169 return 206835 - (1015 * w) / s - (84600 * y) / w
170}
171
172// FKGL x1000 = 390*W/S + 11800*Y/W - 15590 (US grade level)
173func wc_fk_grade(out: *i64) -> i64 {
174 var w: i64 = out[1]
175 var s: i64 = out[2]
176 let y: i64 = out[3]
177 if w < 1 { w = 1 }
178 if s < 1 { s = 1 }
179 return (390 * w) / s + (11800 * y) / w - 15590
180}
181
182// FOG x1000 = 400*W/S + 40000*C/W (Gunning Fog, US grade level)
183func wc_gunning_fog(out: *i64) -> i64 {
184 var w: i64 = out[1]
185 var s: i64 = out[2]
186 let c: i64 = out[4]
187 if w < 1 { w = 1 }
188 if s < 1 { s = 1 }
189 return (400 * w) / s + (40000 * c) / w
190}