gloo 6.3.1 → 6.5.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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +13 -0
  3. data/docs/application.md +9 -0
  4. data/docs/language_objects.md +78 -0
  5. data/docs/language_scripting.md +41 -0
  6. data/docs/objects.md +1 -1
  7. data/docs/verbs.md +36 -2
  8. data/lib/VERSION +1 -1
  9. data/lib/VERSION_NOTES +13 -0
  10. data/lib/gloo/app/engine.rb +5 -1
  11. data/lib/gloo/app/log.rb +5 -5
  12. data/lib/gloo/app/settings.rb +4 -1
  13. data/lib/gloo/core/obj.rb +60 -1
  14. data/lib/gloo/core/parser.rb +30 -1
  15. data/lib/gloo/core/tokens.rb +44 -18
  16. data/lib/gloo/expr/l_string.rb +8 -4
  17. data/lib/gloo/objs/basic/script.rb +28 -2
  18. data/lib/gloo/objs/web/json.rb +4 -1
  19. data/lib/gloo/persist/comment_buffer.rb +58 -0
  20. data/lib/gloo/persist/declaration_ledger.rb +61 -0
  21. data/lib/gloo/persist/disc_mech.rb +15 -2
  22. data/lib/gloo/persist/file_loader.rb +208 -102
  23. data/lib/gloo/persist/file_saver.rb +251 -6
  24. data/lib/gloo/persist/file_storage.rb +10 -4
  25. data/lib/gloo/persist/indent_stack.rb +90 -0
  26. data/lib/gloo/persist/line_splitter.rb +18 -3
  27. data/lib/gloo/persist/persist_man.rb +193 -26
  28. data/lib/gloo/persist/script_body_collector.rb +102 -0
  29. data/lib/gloo/persist/shorthand_expander.rb +51 -0
  30. data/lib/gloo/persist/source/blank_node.rb +27 -0
  31. data/lib/gloo/persist/source/comment_node.rb +27 -0
  32. data/lib/gloo/persist/source/directive_node.rb +26 -0
  33. data/lib/gloo/persist/source/obj_node.rb +91 -0
  34. data/lib/gloo/persist/source/source_doc.rb +41 -0
  35. data/lib/gloo/verbs/list.rb +24 -2
  36. data/lib/gloo/verbs/load.rb +9 -1
  37. data/lib/gloo/verbs/reload.rb +3 -0
  38. data/lib/gloo/verbs/save.rb +57 -10
  39. data/test.gloo/ctrl/each.test.gloo +28 -30
  40. data/test.gloo/dt/date.test.gloo +69 -71
  41. data/test.gloo/dt/datetime.test.gloo +55 -57
  42. data/test.gloo/dt/time.test.gloo +70 -72
  43. data/test.gloo/lang/continuation.test.gloo +25 -27
  44. data/test.gloo/lang/convert.test.gloo +50 -52
  45. data/test.gloo/lang/dt_comparisons.test.gloo +87 -89
  46. data/test.gloo/lang/exceptions.test.gloo +42 -44
  47. data/test.gloo/lang/gloo_sys.test.gloo +100 -102
  48. data/test.gloo/lang/here.test.gloo +32 -34
  49. data/test.gloo/lang/it.test.gloo +27 -29
  50. data/test.gloo/lang/literal.test.gloo +55 -57
  51. data/test.gloo/lang/load_lib_directive.test.gloo +18 -20
  52. data/test.gloo/lang/naming.test.gloo +28 -30
  53. data/test.gloo/lang/ops.test.gloo +53 -55
  54. data/test.gloo/lang/shorthand.test.gloo +27 -0
  55. data/test.gloo/math/add.test.gloo +40 -42
  56. data/test.gloo/math/div.test.gloo +23 -25
  57. data/test.gloo/math/mult.test.gloo +23 -25
  58. data/test.gloo/math/sub.test.gloo +23 -25
  59. data/test.gloo/objs/alias.test.gloo +15 -17
  60. data/test.gloo/objs/bool.test.gloo +54 -56
  61. data/test.gloo/objs/can.test.gloo +33 -35
  62. data/test.gloo/objs/cipher.test.gloo +39 -41
  63. data/test.gloo/objs/decimal.test.gloo +27 -29
  64. data/test.gloo/objs/erb.test.gloo +47 -49
  65. data/test.gloo/objs/file.test.gloo +68 -70
  66. data/test.gloo/objs/function.test.gloo +37 -39
  67. data/test.gloo/objs/int.test.gloo +54 -56
  68. data/test.gloo/objs/json.test.gloo +57 -50
  69. data/test.gloo/objs/obj.test.gloo +68 -48
  70. data/test.gloo/objs/outline.test.gloo +48 -50
  71. data/test.gloo/objs/password.test.gloo +34 -36
  72. data/test.gloo/objs/repeat.test.gloo +42 -44
  73. data/test.gloo/objs/script.test.gloo +13 -15
  74. data/test.gloo/objs/string.test.gloo +82 -84
  75. data/test.gloo/objs/text.test.gloo +90 -92
  76. data/test.gloo/objs/untyped.test.gloo +36 -38
  77. data/test.gloo/objs/uri.test.gloo +37 -39
  78. data/test.gloo/string/str.test.gloo +15 -0
  79. data/test.gloo/verbs/break.test.gloo +12 -14
  80. data/test.gloo/verbs/check.test.gloo +22 -24
  81. data/test.gloo/verbs/context.test.gloo +11 -13
  82. data/test.gloo/verbs/create.test.gloo +7 -9
  83. data/test.gloo/verbs/eval.test.gloo +10 -12
  84. data/test.gloo/verbs/exists.test.gloo +43 -45
  85. data/test.gloo/verbs/if.test.gloo +8 -10
  86. data/test.gloo/verbs/invoke.test.gloo +73 -75
  87. data/test.gloo/verbs/list.test.gloo +21 -23
  88. data/test.gloo/verbs/load.test.gloo +27 -21
  89. data/test.gloo/verbs/log.test.gloo +7 -9
  90. data/test.gloo/verbs/move.test.gloo +12 -14
  91. data/test.gloo/verbs/put.test.gloo +10 -12
  92. data/test.gloo/verbs/reload.test.gloo +55 -0
  93. data/test.gloo/verbs/run.test.gloo +8 -10
  94. data/test.gloo/verbs/save.test.gloo +111 -0
  95. data/test.gloo/verbs/show.test.gloo +26 -28
  96. data/test.gloo/verbs/tell.test.gloo +8 -10
  97. data/test.gloo/verbs/throw.test.gloo +31 -33
  98. data/test.gloo/verbs/unless.test.gloo +13 -15
  99. data/test.gloo/verbs/unload.test.gloo +10 -12
  100. metadata +14 -1
@@ -74,6 +74,7 @@ module Gloo
74
74
  #
75
75
  def show_obj( obj, indent = ' ' )
76
76
  theme = @engine.theme
77
+ show_doc( obj, indent ) if @engine.settings.list_docs
77
78
  if obj.multiline_value? && obj.value_is_array?
78
79
  str = theme.emphasis( "#{indent}#{obj.name}" )
79
80
  str << theme.accent( " [#{obj.type_display}] : " )
@@ -89,6 +90,23 @@ module Gloo
89
90
  end
90
91
  end
91
92
 
93
+ #
94
+ # Show the object's doc (its leading comment, cleaned up) above
95
+ # its listing line, one '#'-prefixed line per line of doc.
96
+ # Silent when the object has none.
97
+ #
98
+ def show_doc( obj, indent )
99
+ return if obj.doc.to_s.strip.empty?
100
+
101
+ theme = @engine.theme
102
+ # split( -1 ), not each_line -- a doc ending in a blank '#' line
103
+ # ends with "\n", and each_line silently drops that trailing
104
+ # empty line rather than yielding it.
105
+ obj.doc.split( "\n", -1 ).each do |line|
106
+ @engine.log.show theme.muted( "#{indent}# #{line}".rstrip )
107
+ end
108
+ end
109
+
92
110
  #
93
111
  # Determine how many levels to show.
94
112
  #
@@ -130,12 +148,16 @@ module Gloo
130
148
  'current context. When a path is provided, it will be ' \
131
149
  'listed instead of the current context. When using context, ' \
132
150
  'the current context will be shown, but when context has ' \
133
- 'not been set, the root will be shown.',
151
+ 'not been set, the root will be shown. When the list_docs ' \
152
+ 'setting is on, any listed object that has a doc (the ' \
153
+ 'comment block declared immediately above it in its source ' \
154
+ 'file) shows it too.',
134
155
  :syntax => [ 'list {path.to.object}' ],
135
156
  :parameters => [
136
157
  '{path.to.object} — Optional path to object that will be listed. When no path is provided, the current context is used.'
137
158
  ],
138
- :result => 'Object and children are listed out in the CLI.',
159
+ :result => 'Object and children are listed out in the CLI. ' \
160
+ 'Doc lines are included when the list_docs setting is on.',
139
161
  :errors => [
140
162
  "#{TARGET_MISSING_ERR}{path.to.object} — The object specified that is to be listed could not be found."
141
163
  ],
@@ -122,8 +122,16 @@ module Gloo
122
122
  :errors => [
123
123
  "#{MISSING_EXPR_ERR} — No expression is provided as parameter to the verb.",
124
124
  "#{UNKNOWN_OPT_ERR} — The reference type given isn't file, ext, or lib.",
125
- "#{WRONG_NUM_ARGS_ERR} — load expects 2 or 3 arguments (the verb, an optional reference type, and the file/ext/lib name)."
125
+ "#{WRONG_NUM_ARGS_ERR} — load expects 2 or 3 arguments (the verb, an optional reference type, and the file/ext/lib name).",
126
+ 'File not found: {name} — The file name given could not be ' \
127
+ 'resolved to a gloo file (checked as a full path, and ' \
128
+ 'relative to the project and gloo root folders).'
126
129
  ],
130
+ :notes => 'Several files can contribute to one container: ' \
131
+ 'declare it in each file and add different children, and ' \
132
+ 'they merge in the heap. If two loaded files declare the ' \
133
+ 'same object with different values, the first one loaded ' \
134
+ 'wins and a warning is logged.',
127
135
  :examples => <<~EXAMPLES.strip
128
136
  > load my/project/file
129
137
  > load my/app/*
@@ -61,6 +61,9 @@ module Gloo
61
61
  'loaded again. Note that re-load does not trigger the ' \
62
62
  'on_load script to run. There is an on_reload message sent ' \
63
63
  'to all open files.',
64
+ :notes => 'If a file has changes that have not been saved, ' \
65
+ 'reloading discards them (a warning is logged, but the ' \
66
+ 'reload still proceeds).',
64
67
  :examples => <<~EXAMPLES.strip
65
68
  > reload
66
69
  EXAMPLES
@@ -10,14 +10,16 @@ module Gloo
10
10
 
11
11
  KEYWORD = 'save'.freeze
12
12
  KEYWORD_SHORT = 'sv'.freeze
13
+ TO = 'to'.freeze
14
+ MISSING_PATH_ERR = "'Save ... to' must include a path!".freeze
13
15
 
14
16
  #
15
17
  # Run the verb.
16
18
  #
17
19
  def run
18
- # TODO: Not currently using folders or keeping
19
- # track of where the object was loaded from.
20
- @engine.persist_man.save @tokens.second
20
+ return @engine.persist_man.save @tokens.second unless @tokens.index_of( TO )
21
+
22
+ run_save_to
21
23
  end
22
24
 
23
25
  #
@@ -34,6 +36,26 @@ module Gloo
34
36
  return KEYWORD_SHORT
35
37
  end
36
38
 
39
+ # ---------------------------------------------------------------------
40
+ # Private functions
41
+ # ---------------------------------------------------------------------
42
+
43
+ private
44
+
45
+ #
46
+ # Run 'save {obj} to {path}'.
47
+ #
48
+ def run_save_to
49
+ name = @tokens.before_token( TO )[ 1 ]
50
+ path = @tokens.after_token( TO )
51
+ unless path
52
+ @engine.err MISSING_PATH_ERR
53
+ return
54
+ end
55
+
56
+ @engine.persist_man.save_to( name, path )
57
+ end
58
+
37
59
  # ---------------------------------------------------------------------
38
60
  # Verb Documentation
39
61
  # ---------------------------------------------------------------------
@@ -45,17 +67,42 @@ module Gloo
45
67
  {
46
68
  :name => KEYWORD,
47
69
  :shortcut => KEYWORD_SHORT,
48
- :description => 'Save a previously loaded object to a .gloo ' \
49
- 'file. The path will be for the root level object that was ' \
50
- 'loaded earlier.',
51
- :syntax => [ 'save {path.to.object}' ],
70
+ :description => 'Save an object to a .gloo file. With no ' \
71
+ 'path, saves every file that owns a declaration in the ' \
72
+ "object's root (its file, or every contributing file for " \
73
+ 'a namespace shared across several files); with no object ' \
74
+ 'at all, saves every open file. An object not yet mapped ' \
75
+ 'to a file is saved fresh, either to a given path or to a ' \
76
+ "default path built from the object's own name.",
77
+ :syntax => [
78
+ 'save',
79
+ 'save {path.to.object}',
80
+ 'save {path.to.object} to {path}'
81
+ ],
52
82
  :parameters => [
53
- '{path.to.object} — Name of the object file that is to be saved.'
83
+ '{path.to.object} — Name of the object to save.',
84
+ '{path} — Where to save it, relative to the project root ' \
85
+ '(.gloo appended if omitted). Registers the mapping, so ' \
86
+ 'a later bare save includes it.'
87
+ ],
88
+ :result => 'The file(s) are updated with the latest object ' \
89
+ 'state. This is a rewrite, not a regeneration: comments, ' \
90
+ 'blank lines, and the original formatting are preserved, ' \
91
+ 'and only values that actually changed are re-written.',
92
+ :errors => [
93
+ "#{MISSING_PATH_ERR} — 'to' was given with nothing after it.",
94
+ 'Could not resolve object to save — the object was not found.',
95
+ 'Will not overwrite a file not already saved there — the ' \
96
+ 'target path exists but is not mapped to this object.'
54
97
  ],
55
- :result => 'The file is updated with the latest object state.',
56
- :notes => 'The save verb is not currently fully functional.',
98
+ :notes => 'An object can also be told to save itself: ' \
99
+ '`tell my_obj to save`. When several files contribute to ' \
100
+ 'one container (the namespace pattern), each file save only ' \
101
+ "rewrites that file's own declarations.",
57
102
  :examples => <<~EXAMPLES.strip
103
+ > save
58
104
  > save my_obj
105
+ > save my_obj to sub/my_obj
59
106
  EXAMPLES
60
107
  }
61
108
  end
@@ -2,38 +2,36 @@
2
2
  # Each and repeat control object tests
3
3
  #
4
4
 
5
- tests [can] :
6
- ctrl [can] :
7
- each [can] :
5
+ tests.ctrl.each [can] :
8
6
 
9
- count [int] : 0
7
+ count [int] : 0
10
8
 
11
- wrd [each] :
12
- word [string] :
13
- IN [string] : one two three
14
- do [script] :
15
- eval ^^.count + 1
16
- put it into ^^.count
9
+ wrd [each] :
10
+ word [string] :
11
+ IN [string] : one two three
12
+ do [script] :
13
+ eval ^^.count + 1
14
+ put it into ^^.count
17
15
 
18
- rpt [repeat] :
19
- times [int] : 3
20
- index [int] : 0
21
- do [script] :
22
- eval ^^.count + 1
23
- put it into ^^.count
16
+ rpt [repeat] :
17
+ times [int] : 3
18
+ index [int] : 0
19
+ do [script] :
20
+ eval ^^.count + 1
21
+ put it into ^^.count
24
22
 
25
- each_word [test] :
26
- description [string] : Count words with each word
27
- on_test [script] :
28
- put 0 into ^^.count
29
- tell ^^.wrd to run
30
- eval ^^.count = 3
31
- assert "expected 3 words counted"
23
+ each_word [test] :
24
+ description [string] : Count words with each word
25
+ on_test [script] :
26
+ put 0 into ^^.count
27
+ tell ^^.wrd to run
28
+ eval ^^.count = 3
29
+ assert "expected 3 words counted"
32
30
 
33
- repeat_n_times [test] :
34
- description [string] : Repeat a script n times
35
- on_test [script] :
36
- put 0 into ^^.count
37
- tell ^^.rpt to run
38
- eval ^^.count = 3
39
- assert "expected repeat to run 3 times"
31
+ repeat_n_times [test] :
32
+ description [string] : Repeat a script n times
33
+ on_test [script] :
34
+ put 0 into ^^.count
35
+ tell ^^.rpt to run
36
+ eval ^^.count = 3
37
+ assert "expected repeat to run 3 times"
@@ -2,87 +2,85 @@
2
2
  # Date tests
3
3
  #
4
4
 
5
- tests [can] :
6
- dt [can] :
7
- date [can] :
5
+ tests.dt.date [can] :
8
6
 
9
- d [date] :
10
- x [date] :
11
- future [date] :
7
+ d [date] :
8
+ x [date] :
9
+ future [date] :
12
10
 
13
- now [test] :
14
- description [string] : Get the current date
15
- on_test [script] :
16
- check ^^.x for blank?
17
- assert "expected new date to be blank"
11
+ now [test] :
12
+ description [string] : Get the current date
13
+ on_test [script] :
14
+ check ^^.x for blank?
15
+ assert "expected new date to be blank"
18
16
 
19
- tell ^^.x to now
20
- check ^^.x for blank?
21
- refute "expected date to not be blank"
17
+ tell ^^.x to now
18
+ check ^^.x for blank?
19
+ refute "expected date to not be blank"
22
20
 
23
- tell ^^.d to now
24
- eval ^^.d = ^^.x
25
- assert "expected dates to be equal"
21
+ tell ^^.d to now
22
+ eval ^^.d = ^^.x
23
+ assert "expected dates to be equal"
26
24
 
27
- add [test] :
28
- description [string] : Add days to a date
29
- on_test [script] :
30
- tell ^^.d to now
31
- tell ^^.future to now
32
- tell ^^.future to add
33
- eval ^^.future = ^^.d
34
- refute "expected future date to be after current date"
25
+ add [test] :
26
+ description [string] : Add days to a date
27
+ on_test [script] :
28
+ tell ^^.d to now
29
+ tell ^^.future to now
30
+ tell ^^.future to add
31
+ eval ^^.future = ^^.d
32
+ refute "expected future date to be after current date"
35
33
 
36
- subtract [test] :
37
- description [string] : Subtract days from a date
38
- on_test [script] :
39
- tell ^^.d to now
40
- tell ^^.future to now
41
- tell ^^.future to sub
42
- eval ^^.future = ^^.d
43
- refute "expected past date to be before current date"
34
+ subtract [test] :
35
+ description [string] : Subtract days from a date
36
+ on_test [script] :
37
+ tell ^^.d to now
38
+ tell ^^.future to now
39
+ tell ^^.future to sub
40
+ eval ^^.future = ^^.d
41
+ refute "expected past date to be before current date"
44
42
 
45
- dd [test] :
46
- description [string] : Get the day portion of a date
47
- on_test [script] :
48
- tell ^^.d to now
49
- tell ^^.d to dd
50
- eval it = ^^.d
51
- refute "expected day to not be the same as the date"
43
+ dd [test] :
44
+ description [string] : Get the day portion of a date
45
+ on_test [script] :
46
+ tell ^^.d to now
47
+ tell ^^.d to dd
48
+ eval it = ^^.d
49
+ refute "expected day to not be the same as the date"
52
50
 
53
- mm [test] :
54
- description [string] : Get the month portion of a date
55
- on_test [script] :
56
- tell ^^.d to now
57
- tell ^^.d to mm
58
- eval it = ^^.d
59
- refute "expected month to not be the same as the date"
51
+ mm [test] :
52
+ description [string] : Get the month portion of a date
53
+ on_test [script] :
54
+ tell ^^.d to now
55
+ tell ^^.d to mm
56
+ eval it = ^^.d
57
+ refute "expected month to not be the same as the date"
60
58
 
61
- yy [test] :
62
- description [string] : Get the year portion of a date
63
- on_test [script] :
64
- tell ^^.d to now
65
- tell ^^.d to yy
66
- eval it = ^^.d
67
- refute "expected year to not be the same as the date"
59
+ yy [test] :
60
+ description [string] : Get the year portion of a date
61
+ on_test [script] :
62
+ tell ^^.d to now
63
+ tell ^^.d to yy
64
+ eval it = ^^.d
65
+ refute "expected year to not be the same as the date"
68
66
 
69
- yyyy [test] :
70
- description [string] : Get the 4 digit year portion of a date
71
- on_test [script] :
72
- tell ^^.d to now
73
- tell ^^.d to yyyy
74
- eval ^^.d = it
75
- refute "expected yyyy to not be the same as the date"
67
+ yyyy [test] :
68
+ description [string] : Get the 4 digit year portion of a date
69
+ on_test [script] :
70
+ tell ^^.d to now
71
+ tell ^^.d to yyyy
72
+ eval ^^.d = it
73
+ refute "expected yyyy to not be the same as the date"
76
74
 
77
- format [test] :
78
- description [string] : Format a date
79
- on_test [script] :
80
- put '2025.10.15' into ^^.d
81
- tell ^^.d to format
82
- eval it = '2025-10-15'
83
- assert "expected formatted date to match"
75
+ format [test] :
76
+ description [string] : Format a date
77
+ on_test [script] :
78
+ put '2025.10.15' into ^^.d
79
+ tell ^^.d to format
80
+ eval it = '2025-10-15'
81
+ assert "expected formatted date to match"
84
82
 
85
- tell ^^.d to format ('%m/%d')
86
- eval it = '10/15'
87
- assert "expected formatted date to match"
83
+ tell ^^.d to format ('%m/%d')
84
+ eval it = '10/15'
85
+ assert "expected formatted date to match"
88
86
 
@@ -2,60 +2,58 @@
2
2
  # Datetime tests
3
3
  #
4
4
 
5
- tests [can] :
6
- dt [can] :
7
- datetime [can] :
8
-
9
- d [dt] :
10
-
11
- now [test] :
12
- description [string] : Get the current datetime
13
- on_test [script] :
14
- check ^^.d for blank?
15
- assert "expected new datetime to be blank"
16
-
17
- tell ^^.d to now
18
- check ^^.d for blank?
19
- refute "expected datetime to not be blank after now"
20
-
21
- is_today [test] :
22
- description [string] : Check if datetime is today
23
- on_test [script] :
24
- tell ^^.d to now
25
- tell ^^.d to is_today
26
- assert "expected current datetime to be today"
27
-
28
- is_past [test] :
29
- description [string] : Check if datetime is in the past
30
- on_test [script] :
31
- tell ^^.d to now
32
- tell ^^.d to is_past
33
- refute "expected current datetime to not be in the past"
34
-
35
- put 'yesterday' into ^^.d
36
- tell ^^.d to is_past
37
- assert "expected yesterday to be in the past"
38
-
39
- is_future [test] :
40
- description [string] : Check if datetime is in the future
41
- on_test [script] :
42
- tell ^^.d to now
43
- tell ^^.d to is_future
44
- refute "expected current datetime to not be in the future"
45
-
46
- put 'tomorrow' into ^^.d
47
- tell ^^.d to is_future
48
- assert "expected tomorrow to be in the future"
49
-
50
- format [test] :
51
- description [string] : Format a datetime
52
- f [dt] :
53
- on_test [script] :
54
- put '2025.10.15 2:30:00 PM' into ^.f
55
- tell ^.f to format
56
- eval it = '2025-10-15 14:30:00'
57
- assert "expected formatted datetime to match"
58
-
59
- tell ^.f to format ('%m/%d/%Y')
60
- eval it = '10/15/2025'
61
- assert "expected formatted datetime to match"
5
+ tests.dt.datetime [can] :
6
+
7
+ d [dt] :
8
+
9
+ now [test] :
10
+ description [string] : Get the current datetime
11
+ on_test [script] :
12
+ check ^^.d for blank?
13
+ assert "expected new datetime to be blank"
14
+
15
+ tell ^^.d to now
16
+ check ^^.d for blank?
17
+ refute "expected datetime to not be blank after now"
18
+
19
+ is_today [test] :
20
+ description [string] : Check if datetime is today
21
+ on_test [script] :
22
+ tell ^^.d to now
23
+ tell ^^.d to is_today
24
+ assert "expected current datetime to be today"
25
+
26
+ is_past [test] :
27
+ description [string] : Check if datetime is in the past
28
+ on_test [script] :
29
+ tell ^^.d to now
30
+ tell ^^.d to is_past
31
+ refute "expected current datetime to not be in the past"
32
+
33
+ put 'yesterday' into ^^.d
34
+ tell ^^.d to is_past
35
+ assert "expected yesterday to be in the past"
36
+
37
+ is_future [test] :
38
+ description [string] : Check if datetime is in the future
39
+ on_test [script] :
40
+ tell ^^.d to now
41
+ tell ^^.d to is_future
42
+ refute "expected current datetime to not be in the future"
43
+
44
+ put 'tomorrow' into ^^.d
45
+ tell ^^.d to is_future
46
+ assert "expected tomorrow to be in the future"
47
+
48
+ format [test] :
49
+ description [string] : Format a datetime
50
+ f [dt] :
51
+ on_test [script] :
52
+ put '2025.10.15 2:30:00 PM' into ^.f
53
+ tell ^.f to format
54
+ eval it = '2025-10-15 14:30:00'
55
+ assert "expected formatted datetime to match"
56
+
57
+ tell ^.f to format ('%m/%d/%Y')
58
+ eval it = '10/15/2025'
59
+ assert "expected formatted datetime to match"