code wiki / (root) / nx_writecraft.nx

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}