parsanol 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of parsanol might be problematic. Click here for more details.

Files changed (336) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +25 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +643 -0
  5. data/Rakefile +189 -0
  6. data/example/balanced-parens/basic.rb +42 -0
  7. data/example/balanced-parens/basic.rb.md +86 -0
  8. data/example/balanced-parens/parens.rb +42 -0
  9. data/example/balanced-parens/ruby_transform.rb +162 -0
  10. data/example/big.erb +73 -0
  11. data/example/boolean-algebra/basic.rb +70 -0
  12. data/example/boolean-algebra/basic.rb.md +108 -0
  13. data/example/boolean-algebra/ruby_transform.rb +263 -0
  14. data/example/calculator/basic.rb +153 -0
  15. data/example/calculator/basic.rb.md +120 -0
  16. data/example/calculator/pattern.rb +153 -0
  17. data/example/calculator/ruby_transform.rb +156 -0
  18. data/example/calculator/ruby_transform.rb.md +32 -0
  19. data/example/calculator/serialized.rb +257 -0
  20. data/example/calculator/serialized.rb.md +32 -0
  21. data/example/calculator/transform.rb +153 -0
  22. data/example/calculator/zero_copy.rb +269 -0
  23. data/example/calculator/zero_copy.rb.md +36 -0
  24. data/example/capture/basic.rb +49 -0
  25. data/example/capture/basic.rb.md +106 -0
  26. data/example/capture/example.json +39 -0
  27. data/example/comments/basic.rb +35 -0
  28. data/example/comments/basic.rb.md +110 -0
  29. data/example/csv/ruby_transform.rb +148 -0
  30. data/example/csv/ruby_transform.rb.md +131 -0
  31. data/example/csv/serialized.rb +201 -0
  32. data/example/csv/serialized.rb.md +31 -0
  33. data/example/csv/zero_copy.rb +276 -0
  34. data/example/csv/zero_copy.rb.md +36 -0
  35. data/example/custom_atoms/indent_atom.rb +79 -0
  36. data/example/deepest-errors/basic.rb +131 -0
  37. data/example/deepest-errors/basic.rb.md +152 -0
  38. data/example/documentation/basic.rb +18 -0
  39. data/example/documentation/basic.rb.md +97 -0
  40. data/example/email/basic.rb +55 -0
  41. data/example/email/basic.rb.md +102 -0
  42. data/example/email/ruby_transform.rb +106 -0
  43. data/example/empty/basic.rb +13 -0
  44. data/example/empty/basic.rb.md +73 -0
  45. data/example/empty/example.json +38 -0
  46. data/example/erb/basic.rb +47 -0
  47. data/example/erb/basic.rb.md +103 -0
  48. data/example/erb/optimized.rb +42 -0
  49. data/example/error-reporting/basic.rb +132 -0
  50. data/example/error-reporting/basic.rb.md +122 -0
  51. data/example/expression-evaluator/basic.rb +284 -0
  52. data/example/expression-evaluator/basic.rb.md +138 -0
  53. data/example/ini/basic.rb +154 -0
  54. data/example/ini/basic.rb.md +129 -0
  55. data/example/ini/ruby_transform.rb +154 -0
  56. data/example/ip-address/basic.rb +125 -0
  57. data/example/ip-address/basic.rb.md +139 -0
  58. data/example/iso-6709/basic.rb +231 -0
  59. data/example/iso-6709/basic.rb.md +143 -0
  60. data/example/iso-8601/basic.rb +275 -0
  61. data/example/iso-8601/basic.rb.md +149 -0
  62. data/example/json/basic.rb +128 -0
  63. data/example/json/basic.rb.md +121 -0
  64. data/example/json/pattern.rb +128 -0
  65. data/example/json/ruby_transform.rb +200 -0
  66. data/example/json/ruby_transform.rb.md +32 -0
  67. data/example/json/serialized.rb +233 -0
  68. data/example/json/serialized.rb.md +31 -0
  69. data/example/json/transform.rb +128 -0
  70. data/example/json/zero_copy.rb +316 -0
  71. data/example/json/zero_copy.rb.md +36 -0
  72. data/example/local/basic.rb +34 -0
  73. data/example/local/basic.rb.md +91 -0
  74. data/example/local/example.json +38 -0
  75. data/example/markdown/basic.rb +287 -0
  76. data/example/markdown/basic.rb.md +160 -0
  77. data/example/markup/basic.rb +173 -0
  78. data/example/markup/basic.rb.md +118 -0
  79. data/example/mathn/basic.rb +47 -0
  80. data/example/mathn/basic.rb.md +96 -0
  81. data/example/mathn/example.json +39 -0
  82. data/example/minilisp/basic.rb +94 -0
  83. data/example/minilisp/basic.rb.md +133 -0
  84. data/example/modularity/basic.rb +47 -0
  85. data/example/modularity/basic.rb.md +152 -0
  86. data/example/nested-errors/basic.rb +132 -0
  87. data/example/nested-errors/basic.rb.md +157 -0
  88. data/example/output/boolean_algebra.out +4 -0
  89. data/example/output/calc.out +1 -0
  90. data/example/output/capture.out +3 -0
  91. data/example/output/comments.out +8 -0
  92. data/example/output/deepest_errors.out +54 -0
  93. data/example/output/documentation.err +4 -0
  94. data/example/output/documentation.out +1 -0
  95. data/example/output/email_parser.out +2 -0
  96. data/example/output/empty.err +1 -0
  97. data/example/output/erb.out +7 -0
  98. data/example/output/ignore.out +1 -0
  99. data/example/output/ignore_whitespace.out +1 -0
  100. data/example/output/ip_address.out +9 -0
  101. data/example/output/json.out +5 -0
  102. data/example/output/local.out +3 -0
  103. data/example/output/mathn.out +4 -0
  104. data/example/output/minilisp.out +5 -0
  105. data/example/output/modularity.out +0 -0
  106. data/example/output/nested_errors.out +54 -0
  107. data/example/output/optimized_erb.out +1 -0
  108. data/example/output/parens.out +8 -0
  109. data/example/output/prec_calc.out +5 -0
  110. data/example/output/readme.out +1 -0
  111. data/example/output/scopes.out +1 -0
  112. data/example/output/seasons.out +28 -0
  113. data/example/output/sentence.out +1 -0
  114. data/example/output/simple_xml.out +2 -0
  115. data/example/output/string_parser.out +3 -0
  116. data/example/prec-calc/basic.rb +71 -0
  117. data/example/prec-calc/basic.rb.md +114 -0
  118. data/example/readme/basic.rb +30 -0
  119. data/example/readme/basic.rb.md +80 -0
  120. data/example/scopes/basic.rb +15 -0
  121. data/example/scopes/basic.rb.md +73 -0
  122. data/example/scopes/example.json +38 -0
  123. data/example/seasons/basic.rb +46 -0
  124. data/example/seasons/basic.rb.md +117 -0
  125. data/example/seasons/example.json +40 -0
  126. data/example/sentence/basic.rb +36 -0
  127. data/example/sentence/basic.rb.md +81 -0
  128. data/example/sexp/ruby_transform.rb +180 -0
  129. data/example/sexp/ruby_transform.rb.md +143 -0
  130. data/example/simple-xml/basic.rb +54 -0
  131. data/example/simple-xml/basic.rb.md +125 -0
  132. data/example/simple.lit +3 -0
  133. data/example/string-literal/basic.rb +77 -0
  134. data/example/string-literal/basic.rb.md +128 -0
  135. data/example/test.lit +4 -0
  136. data/example/toml/basic.rb +226 -0
  137. data/example/toml/basic.rb.md +173 -0
  138. data/example/url/basic.rb +219 -0
  139. data/example/url/basic.rb.md +142 -0
  140. data/example/url/ruby_transform.rb +219 -0
  141. data/example/yaml/basic.rb +216 -0
  142. data/example/yaml/basic.rb.md +148 -0
  143. data/ext/parsanol_native/extconf.rb +4 -0
  144. data/lib/parsanol/accelerator/application.rb +62 -0
  145. data/lib/parsanol/accelerator/engine.rb +112 -0
  146. data/lib/parsanol/accelerator.rb +162 -0
  147. data/lib/parsanol/ast_visitor.rb +122 -0
  148. data/lib/parsanol/atoms/alternative.rb +97 -0
  149. data/lib/parsanol/atoms/base.rb +214 -0
  150. data/lib/parsanol/atoms/can_flatten.rb +192 -0
  151. data/lib/parsanol/atoms/capture.rb +41 -0
  152. data/lib/parsanol/atoms/context.rb +351 -0
  153. data/lib/parsanol/atoms/context_optimized.rb +42 -0
  154. data/lib/parsanol/atoms/custom.rb +110 -0
  155. data/lib/parsanol/atoms/cut.rb +62 -0
  156. data/lib/parsanol/atoms/dsl.rb +130 -0
  157. data/lib/parsanol/atoms/dynamic.rb +33 -0
  158. data/lib/parsanol/atoms/entity.rb +55 -0
  159. data/lib/parsanol/atoms/ignored.rb +28 -0
  160. data/lib/parsanol/atoms/infix.rb +121 -0
  161. data/lib/parsanol/atoms/lookahead.rb +64 -0
  162. data/lib/parsanol/atoms/named.rb +50 -0
  163. data/lib/parsanol/atoms/re.rb +61 -0
  164. data/lib/parsanol/atoms/repetition.rb +241 -0
  165. data/lib/parsanol/atoms/scope.rb +28 -0
  166. data/lib/parsanol/atoms/sequence.rb +157 -0
  167. data/lib/parsanol/atoms/str.rb +90 -0
  168. data/lib/parsanol/atoms/visitor.rb +91 -0
  169. data/lib/parsanol/atoms.rb +36 -0
  170. data/lib/parsanol/buffer.rb +130 -0
  171. data/lib/parsanol/builder_callbacks.rb +353 -0
  172. data/lib/parsanol/cause.rb +101 -0
  173. data/lib/parsanol/context.rb +23 -0
  174. data/lib/parsanol/convenience.rb +35 -0
  175. data/lib/parsanol/edit_tracker.rb +107 -0
  176. data/lib/parsanol/error_reporter/contextual.rb +122 -0
  177. data/lib/parsanol/error_reporter/deepest.rb +106 -0
  178. data/lib/parsanol/error_reporter/tree.rb +68 -0
  179. data/lib/parsanol/error_reporter.rb +98 -0
  180. data/lib/parsanol/export.rb +163 -0
  181. data/lib/parsanol/expression/treetop.rb +94 -0
  182. data/lib/parsanol/expression.rb +51 -0
  183. data/lib/parsanol/fast_mode.rb +145 -0
  184. data/lib/parsanol/first_set.rb +75 -0
  185. data/lib/parsanol/grammar_builder.rb +177 -0
  186. data/lib/parsanol/graphviz.rb +97 -0
  187. data/lib/parsanol/incremental_parser.rb +179 -0
  188. data/lib/parsanol/interval_tree.rb +215 -0
  189. data/lib/parsanol/lazy_result.rb +178 -0
  190. data/lib/parsanol/lexer.rb +146 -0
  191. data/lib/parsanol/native/parser.rb +630 -0
  192. data/lib/parsanol/native/serializer.rb +245 -0
  193. data/lib/parsanol/native/transformer.rb +438 -0
  194. data/lib/parsanol/native/types.rb +41 -0
  195. data/lib/parsanol/native.rb +217 -0
  196. data/lib/parsanol/optimizer.rb +86 -0
  197. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  198. data/lib/parsanol/optimizers/cut_inserter.rb +175 -0
  199. data/lib/parsanol/optimizers/lookahead_optimizer.rb +58 -0
  200. data/lib/parsanol/optimizers/quantifier_optimizer.rb +62 -0
  201. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  202. data/lib/parsanol/options/ruby_transform.rb +109 -0
  203. data/lib/parsanol/options/serialized.rb +94 -0
  204. data/lib/parsanol/options/zero_copy.rb +130 -0
  205. data/lib/parsanol/options.rb +20 -0
  206. data/lib/parsanol/parallel.rb +133 -0
  207. data/lib/parsanol/parsanol_native.bundle +0 -0
  208. data/lib/parsanol/parser.rb +151 -0
  209. data/lib/parsanol/parslet.rb +148 -0
  210. data/lib/parsanol/parslet_native.bundle +0 -0
  211. data/lib/parsanol/pattern/binding.rb +49 -0
  212. data/lib/parsanol/pattern.rb +115 -0
  213. data/lib/parsanol/pool.rb +220 -0
  214. data/lib/parsanol/pools/array_pool.rb +75 -0
  215. data/lib/parsanol/pools/buffer_pool.rb +173 -0
  216. data/lib/parsanol/pools/position_pool.rb +92 -0
  217. data/lib/parsanol/pools/slice_pool.rb +64 -0
  218. data/lib/parsanol/position.rb +89 -0
  219. data/lib/parsanol/result.rb +44 -0
  220. data/lib/parsanol/result_builder.rb +208 -0
  221. data/lib/parsanol/result_stream.rb +262 -0
  222. data/lib/parsanol/rig/rspec.rb +52 -0
  223. data/lib/parsanol/rope.rb +78 -0
  224. data/lib/parsanol/scope.rb +42 -0
  225. data/lib/parsanol/slice.rb +172 -0
  226. data/lib/parsanol/source/line_cache.rb +99 -0
  227. data/lib/parsanol/source.rb +171 -0
  228. data/lib/parsanol/source_location.rb +164 -0
  229. data/lib/parsanol/streaming_parser.rb +124 -0
  230. data/lib/parsanol/string_view.rb +192 -0
  231. data/lib/parsanol/transform.rb +267 -0
  232. data/lib/parsanol/version.rb +5 -0
  233. data/lib/parsanol/wasm/README.md +80 -0
  234. data/lib/parsanol/wasm/package.json +51 -0
  235. data/lib/parsanol/wasm/parsanol.js +252 -0
  236. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  237. data/lib/parsanol/wasm_parser.rb +239 -0
  238. data/lib/parsanol.rb +408 -0
  239. data/parsanol-ruby.gemspec +56 -0
  240. data/spec/acceptance/examples_spec.rb +96 -0
  241. data/spec/acceptance/infix_parser_spec.rb +145 -0
  242. data/spec/acceptance/mixing_parsers_spec.rb +74 -0
  243. data/spec/acceptance/regression_spec.rb +329 -0
  244. data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
  245. data/spec/acceptance/unconsumed_input_spec.rb +21 -0
  246. data/spec/benchmark/comparative/runner_spec.rb +105 -0
  247. data/spec/integration/array_pooling_spec.rb +193 -0
  248. data/spec/integration/buffer_allocation_spec.rb +324 -0
  249. data/spec/integration/position_pooling_spec.rb +184 -0
  250. data/spec/integration/result_builder_spec.rb +282 -0
  251. data/spec/integration/rope_stringview_integration_spec.rb +188 -0
  252. data/spec/integration/slice_pooling_spec.rb +63 -0
  253. data/spec/integration/string_view_integration_spec.rb +125 -0
  254. data/spec/lexer_spec.rb +231 -0
  255. data/spec/parsanol/atom_results_spec.rb +39 -0
  256. data/spec/parsanol/atoms/alternative_spec.rb +26 -0
  257. data/spec/parsanol/atoms/base_spec.rb +127 -0
  258. data/spec/parsanol/atoms/capture_spec.rb +21 -0
  259. data/spec/parsanol/atoms/combinations_spec.rb +5 -0
  260. data/spec/parsanol/atoms/custom_spec.rb +79 -0
  261. data/spec/parsanol/atoms/dsl_spec.rb +7 -0
  262. data/spec/parsanol/atoms/entity_spec.rb +77 -0
  263. data/spec/parsanol/atoms/ignored_spec.rb +15 -0
  264. data/spec/parsanol/atoms/infix_spec.rb +5 -0
  265. data/spec/parsanol/atoms/lookahead_spec.rb +22 -0
  266. data/spec/parsanol/atoms/named_spec.rb +4 -0
  267. data/spec/parsanol/atoms/re_spec.rb +14 -0
  268. data/spec/parsanol/atoms/repetition_spec.rb +24 -0
  269. data/spec/parsanol/atoms/scope_spec.rb +26 -0
  270. data/spec/parsanol/atoms/sequence_spec.rb +28 -0
  271. data/spec/parsanol/atoms/str_spec.rb +15 -0
  272. data/spec/parsanol/atoms/visitor_spec.rb +101 -0
  273. data/spec/parsanol/atoms_spec.rb +488 -0
  274. data/spec/parsanol/auto_optimize_spec.rb +334 -0
  275. data/spec/parsanol/buffer_spec.rb +219 -0
  276. data/spec/parsanol/builder_callbacks_spec.rb +377 -0
  277. data/spec/parsanol/choice_optimizer_spec.rb +231 -0
  278. data/spec/parsanol/convenience_spec.rb +54 -0
  279. data/spec/parsanol/cut_inserter_spec.rb +248 -0
  280. data/spec/parsanol/cut_spec.rb +66 -0
  281. data/spec/parsanol/edit_tracker_spec.rb +218 -0
  282. data/spec/parsanol/error_reporter/contextual_spec.rb +122 -0
  283. data/spec/parsanol/error_reporter/deepest_spec.rb +82 -0
  284. data/spec/parsanol/error_reporter/tree_spec.rb +7 -0
  285. data/spec/parsanol/export_spec.rb +67 -0
  286. data/spec/parsanol/expression/treetop_spec.rb +75 -0
  287. data/spec/parsanol/first_set_spec.rb +298 -0
  288. data/spec/parsanol/interval_tree_spec.rb +205 -0
  289. data/spec/parsanol/lazy_result_spec.rb +288 -0
  290. data/spec/parsanol/lookahead_optimizer_spec.rb +252 -0
  291. data/spec/parsanol/minilisp.citrus +29 -0
  292. data/spec/parsanol/minilisp.tt +29 -0
  293. data/spec/parsanol/optimizer_spec.rb +459 -0
  294. data/spec/parsanol/options/parslet_compat_spec.rb +166 -0
  295. data/spec/parsanol/options/ruby_transform_spec.rb +70 -0
  296. data/spec/parsanol/options/serialized_spec.rb +69 -0
  297. data/spec/parsanol/options/zero_copy_spec.rb +230 -0
  298. data/spec/parsanol/parser_spec.rb +36 -0
  299. data/spec/parsanol/parslet_spec.rb +38 -0
  300. data/spec/parsanol/pattern_spec.rb +272 -0
  301. data/spec/parsanol/pool_spec.rb +392 -0
  302. data/spec/parsanol/pools/array_pool_spec.rb +356 -0
  303. data/spec/parsanol/pools/buffer_pool_spec.rb +365 -0
  304. data/spec/parsanol/pools/position_pool_spec.rb +118 -0
  305. data/spec/parsanol/pools/slice_pool_spec.rb +262 -0
  306. data/spec/parsanol/position_spec.rb +14 -0
  307. data/spec/parsanol/result_builder_spec.rb +391 -0
  308. data/spec/parsanol/rig/rspec_spec.rb +54 -0
  309. data/spec/parsanol/rope_spec.rb +207 -0
  310. data/spec/parsanol/scope_spec.rb +45 -0
  311. data/spec/parsanol/slice_spec.rb +249 -0
  312. data/spec/parsanol/source/line_cache_spec.rb +74 -0
  313. data/spec/parsanol/source_spec.rb +207 -0
  314. data/spec/parsanol/string_view_spec.rb +345 -0
  315. data/spec/parsanol/transform/context_spec.rb +56 -0
  316. data/spec/parsanol/transform_spec.rb +183 -0
  317. data/spec/parsanol/tree_memoization_spec.rb +149 -0
  318. data/spec/parslet_compatibility/expressir_edge_cases_spec.rb +153 -0
  319. data/spec/parslet_compatibility/minimal_reproduction.rb +199 -0
  320. data/spec/parslet_compatibility_spec.rb +399 -0
  321. data/spec/parslet_imported/atom_spec.rb +93 -0
  322. data/spec/parslet_imported/combinator_spec.rb +161 -0
  323. data/spec/parslet_imported/spec_helper.rb +73 -0
  324. data/spec/performance/batch_parsing_benchmark.rb +129 -0
  325. data/spec/performance/complete_optimization_summary.rb +143 -0
  326. data/spec/performance/grammar_caching_analysis.rb +121 -0
  327. data/spec/performance/grammar_caching_benchmark.rb +80 -0
  328. data/spec/performance/native_benchmark_spec.rb +230 -0
  329. data/spec/performance/phase5_benchmark.rb +144 -0
  330. data/spec/performance/profiling_benchmark.rb +131 -0
  331. data/spec/performance/ruby_improvements_benchmark.rb +171 -0
  332. data/spec/performance_spec.rb +374 -0
  333. data/spec/spec_helper.rb +79 -0
  334. data/spec/support/opal.rb +8 -0
  335. data/spec/support/opal.rb.erb +14 -0
  336. metadata +485 -0
@@ -0,0 +1,231 @@
1
+ # ISO 6709 Geographic Coordinate Parser - Ruby Implementation
2
+ #
3
+ # Parse ISO 6709 geographic point locations (latitude, longitude, altitude).
4
+ #
5
+ # Run with: ruby example/iso-6709/basic.rb
6
+
7
+ $:.unshift File.dirname(__FILE__) + "/../lib"
8
+
9
+ require 'parsanol/parslet'
10
+
11
+ # ISO 6709 coordinate parser
12
+ class Iso6709Parser < Parsanol::Parser
13
+ root :coordinate
14
+
15
+ # Sign: + for N/E, - for S/W
16
+ rule(:lat_sign) { (str('+') | str('-')).as(:lat_sign) }
17
+ rule(:lon_sign) { (str('+') | str('-')).as(:lon_sign) }
18
+
19
+ # Decimal degrees: DD.DDDD or DDD.DDDD
20
+ rule(:decimal_deg) {
21
+ match('[0-9]').repeat(1, 2).as(:degrees) >>
22
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:fraction)
23
+ }
24
+
25
+ rule(:decimal_deg_3) {
26
+ match('[0-9]').repeat(1, 3).as(:degrees) >>
27
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:fraction)
28
+ }
29
+
30
+ # Sexagesimal (DMS): DD MM SS.ss or DD MM
31
+ rule(:sexagesimal) {
32
+ match('[0-9]').repeat(1, 2).as(:degrees) >>
33
+ (
34
+ space >>
35
+ match('[0-9]').repeat(1, 2).as(:minutes) >>
36
+ (
37
+ space >>
38
+ match('[0-9]').repeat(1, 2).as(:seconds) >>
39
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:sec_fraction)
40
+ ).maybe
41
+ ).maybe
42
+ }
43
+
44
+ rule(:sexagesimal_3) {
45
+ match('[0-9]').repeat(1, 3).as(:degrees) >>
46
+ (
47
+ space >>
48
+ match('[0-9]').repeat(1, 2).as(:minutes) >>
49
+ (
50
+ space >>
51
+ match('[0-9]').repeat(1, 2).as(:seconds) >>
52
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:sec_fraction)
53
+ ).maybe
54
+ ).maybe
55
+ }
56
+
57
+ # Latitude: -90 to +90
58
+ rule(:latitude) {
59
+ lat_sign >> (decimal_deg | sexagesimal).as(:latitude)
60
+ }
61
+
62
+ # Longitude: -180 to +180
63
+ rule(:longitude) {
64
+ lon_sign >> (decimal_deg_3 | sexagesimal_3).as(:longitude)
65
+ }
66
+
67
+ # Altitude (optional): +AAA.A or -AAA.A
68
+ rule(:altitude) {
69
+ (str('+') | str('-')).as(:alt_sign) >>
70
+ match('[0-9]').repeat(1).as(:alt_value) >>
71
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:alt_fraction)
72
+ }
73
+
74
+ # Coordinate Reference System (optional): CRScode/
75
+ rule(:crs) {
76
+ str('CRS') >>
77
+ match('[A-Z0-9_]').repeat(1).as(:crs) >>
78
+ str('/')
79
+ }
80
+
81
+ # Complete coordinate
82
+ rule(:coordinate) {
83
+ latitude >>
84
+ (space | str('')).maybe >>
85
+ longitude >>
86
+ altitude.maybe.as(:altitude) >>
87
+ (str('/') >> crs).maybe.as(:crs_info)
88
+ }
89
+
90
+ rule(:space) { match('\s') }
91
+ end
92
+
93
+ # Coordinate result class
94
+ Coordinate = Struct.new(:lat_sign, :latitude, :lon_sign, :longitude, :altitude, :crs) do
95
+ def to_h
96
+ {
97
+ latitude: lat_value,
98
+ longitude: lon_value,
99
+ altitude: alt_value,
100
+ crs: crs
101
+ }.compact
102
+ end
103
+
104
+ def lat_value
105
+ return nil unless latitude
106
+ val = degrees_to_decimal(latitude)
107
+ lat_sign == '-' ? -val : val
108
+ end
109
+
110
+ def lon_value
111
+ return nil unless longitude
112
+ val = degrees_to_decimal(longitude)
113
+ lon_sign == '-' ? -val : val
114
+ end
115
+
116
+ def alt_value
117
+ return nil unless altitude
118
+ val = altitude[:alt_value].to_f
119
+ val += altitude[:alt_fraction].to_s.to_f if altitude[:alt_fraction]
120
+ altitude[:alt_sign] == '-' ? -val : val
121
+ end
122
+
123
+ private
124
+
125
+ def degrees_to_decimal(d)
126
+ return 0.0 unless d
127
+
128
+ deg = d[:degrees].to_i
129
+ min = d[:minutes].to_s.to_i
130
+ sec = d[:seconds].to_s.to_f
131
+ sec += d[:sec_fraction].to_s.to_f if d[:sec_fraction]
132
+ frac = d[:fraction].to_s.to_f
133
+
134
+ deg + (min / 60.0) + (sec / 3600.0) + frac
135
+ end
136
+ end
137
+
138
+ # Transform parse tree to Coordinate
139
+ class Iso6709Transform < Parsanol::Transform
140
+ rule(
141
+ lat_sign: simple(:ls),
142
+ latitude: simple(:lat),
143
+ lon_sign: simple(:lons),
144
+ longitude: simple(:lon)
145
+ ) {
146
+ Coordinate.new(ls.to_s, lat, lons.to_s, lon, nil, nil)
147
+ }
148
+
149
+ rule(
150
+ lat_sign: simple(:ls),
151
+ latitude: simple(:lat),
152
+ lon_sign: simple(:lons),
153
+ longitude: simple(:lon),
154
+ altitude: simple(:alt)
155
+ ) {
156
+ Coordinate.new(ls.to_s, lat, lons.to_s, lon, alt, nil)
157
+ }
158
+
159
+ rule(
160
+ lat_sign: simple(:ls),
161
+ latitude: simple(:lat),
162
+ lon_sign: simple(:lons),
163
+ longitude: simple(:lon),
164
+ crs_info: simple(:crs)
165
+ ) {
166
+ Coordinate.new(ls.to_s, lat, lons.to_s, lon, nil, crs.to_s)
167
+ }
168
+
169
+ rule(
170
+ lat_sign: simple(:ls),
171
+ latitude: simple(:lat),
172
+ lon_sign: simple(:lons),
173
+ longitude: simple(:lon),
174
+ altitude: simple(:alt),
175
+ crs_info: simple(:crs)
176
+ ) {
177
+ Coordinate.new(ls.to_s, lat, lons.to_s, lon, alt, crs.to_s)
178
+ }
179
+ end
180
+
181
+ # Parse and return Coordinate
182
+ def parse_coordinate(str)
183
+ parser = Iso6709Parser.new
184
+ transform = Iso6709Transform.new
185
+
186
+ tree = parser.parse(str)
187
+ transform.apply(tree)
188
+ rescue Parsanol::ParseError => e
189
+ puts "Parse error: #{e.message}"
190
+ nil
191
+ end
192
+
193
+ # Main demo
194
+ if __FILE__ == $0
195
+ puts "ISO 6709 Geographic Coordinate Parser"
196
+ puts "=" * 50
197
+ puts
198
+
199
+ coordinates = [
200
+ "+40.6894-074.0447", # Statue of Liberty
201
+ "+48.8584+002.2945", # Eiffel Tower
202
+ "-90+000", # South Pole
203
+ "+27.9881+086.9250", # Mount Everest
204
+ "+40 41 21.84-074 02 40.92", # Sexagesimal format
205
+ "+48.8584+002.2945+330CRSWGS_84/", # With altitude and CRS
206
+ ]
207
+
208
+ coordinates.each do |coord_str|
209
+ puts "Input: #{coord_str}"
210
+ result = parse_coordinate(coord_str)
211
+ if result
212
+ puts " Latitude: #{result.lat_value}"
213
+ puts " Longitude: #{result.lon_value}"
214
+ puts " Altitude: #{result.alt_value}" if result.alt_value
215
+ puts " CRS: #{result.crs}" if result.crs
216
+ end
217
+ puts
218
+ end
219
+
220
+ # Validation examples
221
+ puts "-" * 50
222
+ puts "Validation examples:"
223
+ puts
224
+
225
+ ["+95-074", "+40.6894"].each do |invalid|
226
+ puts "Invalid: #{invalid}"
227
+ result = parse_coordinate(invalid)
228
+ puts " Result: #{result.inspect}"
229
+ puts
230
+ end
231
+ end
@@ -0,0 +1,143 @@
1
+ # ISO 6709 Geographic Coordinate Parser - Ruby Implementation
2
+
3
+ ## How to Run
4
+
5
+ ```bash
6
+ cd parsanol-ruby/example/iso-6709
7
+ ruby basic.rb
8
+ ```
9
+
10
+ ## Code Walkthrough
11
+
12
+ ### Sign Convention
13
+
14
+ Latitude and longitude use signed notation:
15
+
16
+ ```ruby
17
+ rule(:lat_sign) { (str('+') | str('-')).as(:lat_sign) }
18
+ rule(:lon_sign) { (str('+') | str('-')).as(:lon_sign) }
19
+ ```
20
+
21
+ Positive (+) means North/East; negative (-) means South/West.
22
+
23
+ ### Decimal Degrees
24
+
25
+ Simple decimal format captures degrees and optional fraction:
26
+
27
+ ```ruby
28
+ rule(:decimal_deg) {
29
+ match('[0-9]').repeat(1, 2).as(:degrees) >>
30
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:fraction)
31
+ }
32
+ ```
33
+
34
+ Latitude uses 1-2 digits (0-90); longitude uses 1-3 digits (0-180).
35
+
36
+ ### Sexagesimal (DMS) Format
37
+
38
+ Degrees, minutes, and seconds with optional fractions:
39
+
40
+ ```ruby
41
+ rule(:sexagesimal) {
42
+ match('[0-9]').repeat(1, 2).as(:degrees) >>
43
+ (
44
+ space >>
45
+ match('[0-9]').repeat(1, 2).as(:minutes) >>
46
+ (
47
+ space >>
48
+ match('[0-9]').repeat(1, 2).as(:seconds) >>
49
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:sec_fraction)
50
+ ).maybe
51
+ ).maybe
52
+ }
53
+ ```
54
+
55
+ Each component is optional, supporting `DD`, `DD MM`, or `DD MM SS.ss`.
56
+
57
+ ### Latitude and Longitude Rules
58
+
59
+ Latitude is limited to ±90°:
60
+
61
+ ```ruby
62
+ rule(:latitude) {
63
+ lat_sign >> (decimal_deg | sexagesimal).as(:latitude)
64
+ }
65
+ ```
66
+
67
+ Longitude extends to ±180°:
68
+
69
+ ```ruby
70
+ rule(:longitude) {
71
+ lon_sign >> (decimal_deg_3 | sexagesimal_3).as(:longitude)
72
+ }
73
+ ```
74
+
75
+ ### Altitude (Optional)
76
+
77
+ Altitude in meters with sign:
78
+
79
+ ```ruby
80
+ rule(:altitude) {
81
+ (str('+') | str('-')).as(:alt_sign) >>
82
+ match('[0-9]').repeat(1).as(:alt_value) >>
83
+ (str('.') >> match('[0-9]').repeat(1)).maybe.as(:alt_fraction)
84
+ }
85
+ ```
86
+
87
+ Positive is above sea level; negative is below.
88
+
89
+ ### Coordinate Reference System
90
+
91
+ CRS specifies the reference system:
92
+
93
+ ```ruby
94
+ rule(:crs) {
95
+ str('CRS') >>
96
+ match('[A-Z0-9_]').repeat(1).as(:crs) >>
97
+ str('/')
98
+ }
99
+ ```
100
+
101
+ Common values: `WGS_84`, `NAD83`.
102
+
103
+ ### Complete Coordinate
104
+
105
+ All components assembled:
106
+
107
+ ```ruby
108
+ rule(:coordinate) {
109
+ latitude >>
110
+ (space | str('')).maybe >>
111
+ longitude >>
112
+ altitude.maybe.as(:altitude) >>
113
+ (str('/') >> crs).maybe.as(:crs_info)
114
+ }
115
+ ```
116
+
117
+ Altitude and CRS are optional.
118
+
119
+ ## Output Types
120
+
121
+ ```ruby
122
+ # Decimal degrees
123
+ Coordinate.new("+", {:degrees=>"40", :fraction=>".6894"}, "-", {:degrees=>"074", :fraction=>".0447"}, nil, nil)
124
+ # to_h => {:latitude=>40.6894, :longitude=>-74.0447}
125
+
126
+ # With altitude and CRS
127
+ Coordinate.new("+", lat_hash, "+", lon_hash, {:alt_sign=>"+", :alt_value=>"330"}, "WGS_84")
128
+ # to_h => {:latitude=>48.8584, :longitude=>2.2945, :altitude=>330.0, :crs=>"WGS_84"}
129
+ ```
130
+
131
+ ## Design Decisions
132
+
133
+ ### Why Separate Latitude/Longitude Rules?
134
+
135
+ Different valid ranges (±90 vs ±180) require different digit constraints. Separate rules enforce format correctness.
136
+
137
+ ### Why Struct with to_h?
138
+
139
+ Struct provides clean attribute access while `to_h` gives a simple hash representation for serialization.
140
+
141
+ ### Why Maybe for Optional Components?
142
+
143
+ ISO 6709 allows coordinates without altitude or CRS. Using `.maybe` keeps the grammar flexible while still capturing data when present.
@@ -0,0 +1,275 @@
1
+ # ISO 8601 Date/Time Parser - Ruby Implementation
2
+ #
3
+ # Parse ISO 8601 dates, times, datetimes, and durations.
4
+ #
5
+ # Run with: ruby example/iso-8601/basic.rb
6
+
7
+ $:.unshift File.dirname(__FILE__) + "/../lib"
8
+
9
+ require 'parsanol/parslet'
10
+
11
+ # ISO 8601 parser
12
+ class Iso8601Parser < Parsanol::Parser
13
+ root :iso_value
14
+
15
+ # Date components
16
+ rule(:year) { match('[0-9]').repeat(4, 4).as(:year) }
17
+ rule(:month) { match('[0-9]').repeat(2, 2).as(:month) }
18
+ rule(:day) { match('[0-9]').repeat(2, 2).as(:day) }
19
+ rule(:date_separator) { str('-').maybe }
20
+
21
+ # Calendar date: YYYY-MM-DD or YYYYMMDD
22
+ rule(:calendar_date) {
23
+ year >> date_separator >> month >> date_separator >> day
24
+ }
25
+
26
+ # Week date: YYYY-Www-D
27
+ rule(:week_date) {
28
+ year >> str('-W') >>
29
+ match('[0-9]').repeat(2, 2).as(:week) >>
30
+ str('-') >>
31
+ match('[1-7]').as(:weekday)
32
+ }
33
+
34
+ # Ordinal date: YYYY-DDD
35
+ rule(:ordinal_date) {
36
+ year >> str('-') >>
37
+ match('[0-9]').repeat(3, 3).as(:ordinal_day)
38
+ }
39
+
40
+ # Time components
41
+ rule(:hour) { match('[0-9]').repeat(2, 2).as(:hour) }
42
+ rule(:minute) { match('[0-9]').repeat(2, 2).as(:minute) }
43
+ rule(:second) { match('[0-9]').repeat(2, 2).as(:second) }
44
+ rule(:fraction) { str('.') >> match('[0-9]').repeat(1).as(:fraction) }
45
+ rule(:time_separator) { str(':').maybe }
46
+
47
+ # Time: HH:MM:SS[.frac]
48
+ rule(:time_basic) {
49
+ hour >> time_separator >> minute >> time_separator >> second >> fraction.maybe
50
+ }
51
+
52
+ # Timezone
53
+ rule(:utc_designator) { str('Z').as(:utc) }
54
+ rule(:tz_sign) { (str('+') | str('-')).as(:tz_sign) }
55
+ rule(:tz_hour) { match('[0-9]').repeat(2, 2).as(:tz_hour) }
56
+ rule(:tz_minute) { (str(':') >> match('[0-9]').repeat(2, 2)).maybe.as(:tz_minute) }
57
+
58
+ rule(:tz_offset) {
59
+ tz_sign >> tz_hour >> tz_minute
60
+ }
61
+
62
+ rule(:timezone) { utc_designator | tz_offset | str('') }
63
+
64
+ rule(:time) { time_basic >> timezone }
65
+
66
+ # Combined date-time
67
+ rule(:datetime) {
68
+ (calendar_date | week_date | ordinal_date) >>
69
+ (str('T') | str(' ')) >>
70
+ time
71
+ }
72
+
73
+ # Duration: P[nY][nM][nD][T[nH][nM][nS]]
74
+ rule(:duration) {
75
+ str('P') >>
76
+ (
77
+ (match('[0-9]').repeat(1).as(:years) >> str('Y')).maybe >>
78
+ (match('[0-9]').repeat(1).as(:months) >> str('M')).maybe >>
79
+ (match('[0-9]').repeat(1).as(:days) >> str('D')).maybe >>
80
+ (
81
+ str('T') >>
82
+ (
83
+ (match('[0-9]').repeat(1).as(:hours) >> str('H')).maybe >>
84
+ (match('[0-9]').repeat(1).as(:minutes) >> str('M')).maybe >>
85
+ (match('[0-9]').repeat(1).as(:seconds) >> str('S')).maybe
86
+ )
87
+ ).maybe
88
+ )
89
+ }
90
+
91
+ # Top-level alternatives
92
+ rule(:iso_value) { datetime | calendar_date | time | duration }
93
+ end
94
+
95
+ # Date result class
96
+ IsoDate = Struct.new(:year, :month, :day, :week, :weekday, :ordinal_day) do
97
+ def to_s
98
+ if week
99
+ "#{year}-W#{week}-#{weekday}"
100
+ elsif ordinal_day
101
+ "#{year}-#{ordinal_day}"
102
+ else
103
+ "#{year}-#{month}-#{day}"
104
+ end
105
+ end
106
+
107
+ def to_date
108
+ require 'date'
109
+ if week
110
+ Date.commercial(year.to_i, week.to_i, weekday.to_i)
111
+ elsif ordinal_day
112
+ Date.ordinal(year.to_i, ordinal_day.to_i)
113
+ else
114
+ Date.new(year.to_i, month.to_i, day.to_i)
115
+ end
116
+ end
117
+ end
118
+
119
+ # Time result class
120
+ IsoTime = Struct.new(:hour, :minute, :second, :fraction, :utc, :tz_sign, :tz_hour, :tz_minute) do
121
+ def to_s
122
+ h = "#{hour}:#{minute}:#{second}"
123
+ h += ".#{fraction}" if fraction
124
+ h += "Z" if utc
125
+ h += "#{tz_sign}#{tz_hour}#{tz_minute}" if tz_hour
126
+ h
127
+ end
128
+ end
129
+
130
+ # DateTime result class
131
+ IsoDateTime = Struct.new(:date, :time) do
132
+ def to_s
133
+ "#{date}T#{time}"
134
+ end
135
+ end
136
+
137
+ # Duration result class
138
+ IsoDuration = Struct.new(:years, :months, :days, :hours, :minutes, :seconds) do
139
+ def to_s
140
+ parts = ["P"]
141
+ parts << "#{years}Y" if years
142
+ parts << "#{months}M" if months
143
+ parts << "#{days}D" if days
144
+
145
+ time_parts = []
146
+ time_parts << "#{hours}H" if hours
147
+ time_parts << "#{minutes}M" if minutes
148
+ time_parts << "#{seconds}S" if seconds
149
+
150
+ if time_parts.any?
151
+ parts << "T"
152
+ parts.concat(time_parts)
153
+ end
154
+
155
+ parts.join
156
+ end
157
+
158
+ def to_seconds
159
+ total = 0
160
+ total += years.to_i * 365.25 * 24 * 3600 if years
161
+ total += months.to_i * 30.44 * 24 * 3600 if months
162
+ total += days.to_i * 24 * 3600 if days
163
+ total += hours.to_i * 3600 if hours
164
+ total += minutes.to_i * 60 if minutes
165
+ total += seconds.to_i if seconds
166
+ total.to_i
167
+ end
168
+ end
169
+
170
+ # Transform parse tree to result objects
171
+ class Iso8601Transform < Parsanol::Transform
172
+ # Calendar date
173
+ rule(year: simple(:y), month: simple(:m), day: simple(:d)) {
174
+ IsoDate.new(y.to_s, m.to_s, d.to_s, nil, nil, nil)
175
+ }
176
+
177
+ # Week date
178
+ rule(year: simple(:y), week: simple(:w), weekday: simple(:wd)) {
179
+ IsoDate.new(y.to_s, nil, nil, w.to_s, wd.to_s, nil)
180
+ }
181
+
182
+ # Ordinal date
183
+ rule(year: simple(:y), ordinal_day: simple(:od)) {
184
+ IsoDate.new(y.to_s, nil, nil, nil, nil, od.to_s)
185
+ }
186
+
187
+ # Time
188
+ rule(hour: simple(:h), minute: simple(:m), second: simple(:s)) {
189
+ IsoTime.new(h.to_s, m.to_s, s.to_s, nil, nil, nil, nil)
190
+ }
191
+
192
+ rule(hour: simple(:h), minute: simple(:m), second: simple(:s), fraction: simple(:f)) {
193
+ IsoTime.new(h.to_s, m.to_s, s.to_s, f.to_s, nil, nil, nil)
194
+ }
195
+
196
+ rule(hour: simple(:h), minute: simple(:m), second: simple(:s), utc: simple(:u)) {
197
+ IsoTime.new(h.to_s, m.to_s, s.to_s, nil, u.to_s, nil, nil)
198
+ }
199
+
200
+ rule(hour: simple(:h), minute: simple(:m), second: simple(:s), tz_sign: simple(:ts), tz_hour: simple(:th)) {
201
+ IsoTime.new(h.to_s, m.to_s, s.to_s, nil, nil, ts.to_s, th.to_s, nil)
202
+ }
203
+
204
+ # Duration
205
+ rule(
206
+ years: simple(:y), months: simple(:mo), days: simple(:d),
207
+ hours: simple(:h), minutes: simple(:mi), seconds: simple(:s)
208
+ ) {
209
+ IsoDuration.new(y.to_s, mo.to_s, d.to_s, h.to_s, mi.to_s, s.to_s)
210
+ }
211
+ end
212
+
213
+ # Parse ISO 8601 string
214
+ def parse_iso8601(str)
215
+ parser = Iso8601Parser.new
216
+ transform = Iso8601Transform.new
217
+
218
+ tree = parser.parse(str)
219
+ transform.apply(tree)
220
+ rescue Parsanol::ParseError => e
221
+ puts "Parse error: #{e.message}"
222
+ nil
223
+ end
224
+
225
+ # Main demo
226
+ if __FILE__ == $0
227
+ puts "ISO 8601 Date/Time Parser"
228
+ puts "=" * 50
229
+ puts
230
+
231
+ examples = [
232
+ # Calendar dates
233
+ ["2024-01-15", "Calendar date"],
234
+ ["20240115", "Compact date"],
235
+ ["2024-12-25", "Christmas"],
236
+
237
+ # Week dates
238
+ ["2024-W02-1", "Week date (2nd week, Monday)"],
239
+
240
+ # Ordinal dates
241
+ ["2024-015", "Ordinal date (15th day)"],
242
+
243
+ # Times
244
+ ["10:30:00", "Time"],
245
+ ["10:30:00.123", "Time with fraction"],
246
+ ["10:30:00Z", "UTC time"],
247
+ ["10:30:00+09:00", "Time with timezone"],
248
+
249
+ # Date-times
250
+ ["2024-01-15T10:30:00Z", "DateTime UTC"],
251
+ ["2024-01-15T10:30:00+09:00", "DateTime with timezone"],
252
+
253
+ # Durations
254
+ ["P1Y2M3DT4H5M6S", "Full duration"],
255
+ ["PT30M", "30 minutes duration"],
256
+ ["P1D", "1 day duration"],
257
+ ]
258
+
259
+ examples.each do |input, description|
260
+ puts "#{description}:"
261
+ puts " Input: #{input}"
262
+ result = parse_iso8601(input)
263
+ if result
264
+ puts " Result: #{result.inspect}"
265
+ puts " String: #{result}"
266
+ if result.respond_to?(:to_date)
267
+ puts " Date: #{result.to_date rescue 'N/A'}"
268
+ end
269
+ if result.respond_to?(:to_seconds)
270
+ puts " Seconds: #{result.to_seconds}"
271
+ end
272
+ end
273
+ puts
274
+ end
275
+ end