├── GoT
├── GoTCumulative.gif
├── README.md
├── gameofthrones_characters.sas
└── got_gridded.jpg
├── LICENSE
├── NSDUH
├── MJ_use_SAMHDA_ALLSTATES.sas
├── MJ_use_SAMHDA_onestate.sas
└── README.md
├── README.md
├── census-2020
└── state-apportionment-2020-sgmap.sas
├── currency
└── currency.sas
├── emojis
└── emoji-sequences.sas
├── import-xls
└── np_info.xlsx
├── includesasnb
├── README.md
└── includesasnb.sas
├── leadingzeros
└── leadingzeros.sas
├── misc
└── encodedheart.sas
├── msteams
├── README.md
└── webhook_publish.sas
├── netflix-trip
└── NetflixTrip.sas
├── onedrive
├── README.md
├── config.json
├── onedrive_config.sas
├── onedrive_example_use.sas
├── onedrive_macros.sas
├── onedrive_setup.sas
└── onedrive_sharepoint_example.sas
├── precision
├── README.md
├── precision-output.html
└── precision_example.sas
├── prochttp
├── basicauth.sas
├── cms_nursinghome.sas
├── currency.sas
├── httppost.sas
├── prochttp_test.sas
├── webscrape.sas
└── whoseinspace.sas
├── rng_example_thanos.sas
├── rss-feeds
├── README.md
├── blogspot-json.sas
└── wordpress-xml.sas
└── splitfile
└── splitfile.sas
/GoT/GoTCumulative.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sascommunities/sas-dummy-blog/e883da18132d59201195709d81bf8e090e943416/GoT/GoTCumulative.gif
--------------------------------------------------------------------------------
/GoT/README.md:
--------------------------------------------------------------------------------
1 | # Visualization of screen time
2 |
3 | Inspired [by this tweet](https://twitter.com/katie_jean2379/status/1118714039356592128), I built this sample program in SAS.
4 |
5 | Special thanks to Jeffrey Lancaster [for sharing the original data and visualization](https://github.com/jeffreylancaster/game-of-thrones).
6 | Using PROC HTTP and the JSON libname engine, SAS can ingest the data directly with just a few lines of code.
7 |
8 | From there, it's data prep (of course) and then the visualization steps. See code comments for the details.
9 |
10 | Sample grid of Episode-by-episode numbers:
11 |
12 | 
13 |
14 | And the final animation (GIF here, but program produces an SVG).
15 |
16 | 
17 |
--------------------------------------------------------------------------------
/GoT/gameofthrones_characters.sas:
--------------------------------------------------------------------------------
1 | filename eps temp;
2 |
3 | /* Big thanks to this GoT data nerd for assembling this data */
4 | proc http
5 | url="https://raw.githubusercontent.com/jeffreylancaster/game-of-thrones/master/data/episodes.json"
6 | out=eps
7 | method="GET";
8 | run;
9 |
10 | /* slurp this in with the JSON engine */
11 | libname episode JSON fileref=eps;
12 |
13 | /* Build details of scenes and characters who appear in them */
14 | PROC SQL;
15 | CREATE TABLE WORK.character_scenes AS
16 | SELECT t1.seasonNum,
17 | t1.episodeNum,
18 | t2.ordinal_scenes as scene_id,
19 | input(t2.sceneStart,time.) as time_start format=time.,
20 | input(t2.sceneEnd,time.) as time_end format=time.,
21 | (calculated time_end) - (calculated time_start) as duration format=time.,
22 | t3.name
23 | FROM EPISODE.EPISODES t1,
24 | EPISODE.EPISODES_SCENES t2,
25 | EPISODE.SCENES_CHARACTERS t3
26 | WHERE (t1.ordinal_episodes = t2.ordinal_episodes AND
27 | t2.ordinal_scenes = t3.ordinal_scenes);
28 | QUIT;
29 |
30 | /* Create a table of characters and TOTAL screen time */
31 | proc sql;
32 | create table characters as
33 | select name,
34 | sum(duration) as total_screen_time format=time.
35 | from character_scenes
36 | group by name
37 | order by total_screen_time desc;
38 |
39 | /* and a table of scenes */
40 | create table scenes as
41 | select distinct t1.seasonNum, t1.episodeNum, t1.scene_id,
42 | t1.time_start, t1.time_end, t1.duration format=time.,
43 | t2.location, t2.subLocation
44 | from character_scenes t1 left join episode.episodes_scenes t2
45 | on (t1.scene_id = t2.ordinal_scenes);
46 | quit;
47 |
48 | /* DATA prep for per-episode character rankings */
49 | /* Sum up the screen time per character, per episode */
50 | PROC SQL;
51 | CREATE TABLE WORK.per_episode AS
52 | SELECT t1.seasonNum,
53 | t1.episodeNum,
54 | /* timePerEpisode */
55 | (SUM(t1.duration)) AS timePerEpisode,
56 | t1.name,
57 | cat("Season ",seasonNum,", Ep ",episodeNum) as epLabel
58 | FROM WORK.CHARACTER_SCENES t1
59 | GROUP BY t1.seasonNum,
60 | t1.episodeNum,
61 | t1.name
62 | order by seasonNum, episodeNum, name;
63 | QUIT;
64 |
65 | /* Assign ranks so we can filter to just top 10 per episode */
66 | PROC RANK DATA = WORK.per_episode
67 | DESCENDING
68 | TIES=MEAN
69 | OUT=ranked_timings;
70 | BY seasonNum episodeNum;
71 | VAR timePerEpisode;
72 | RANKS rank;
73 |
74 | /* Data prep to assemble cumulative timings ACROSS episodes/seasons */
75 | /* First SORT by name, season, episode */
76 | proc sort data=per_episode
77 | out=for_cumulative;
78 | by name seasonNum episodeNum;
79 | run;
80 |
81 | /* Then use FIRST-dot-NAME processing */
82 | /* plus RETAIN to calc cumulative time per */
83 | /* character name */
84 | data cumulative;
85 | set for_cumulative;
86 | length cumulative 8;
87 | retain cumulative;
88 | by name;
89 | if first.name then cumulative=timePerEpisode;
90 | else cumulative + timePerEpisode;
91 | run;
92 |
93 | /* Now rank the cumulative times PER character PER episode */
94 | /* So that we can report on the top 10 cumulative-time characters */
95 | /* for each episode */
96 | proc sort data=cumulative;
97 | by seasonNum episodeNum descending cumulative ;
98 | run;
99 |
100 | /* Assign ranks so we can filter to just top 10 per episode */
101 | PROC RANK DATA = WORK.cumulative
102 | DESCENDING
103 | TIES=MEAN
104 | OUT=ranked_cumulative;
105 | BY seasonNum episodeNum;
106 | VAR cumulative;
107 | RANKS rank;
108 |
109 | proc sql;
110 | create table all_times
111 | as select t1.*, t2.total_screen_time
112 | from ranked_cumulative t1 left join characters t2 on (t1.name=t2.name)
113 | order by epLabel;
114 | quit;
115 |
116 |
117 | /* Create a gridded presentation of Episode graphs, single ep timings */
118 | title;
119 | filename per_ep temp;
120 | ods graphics / width=500 height=300 imagefmt=svg noborder;
121 | ods html5 file=per_ep options(svg_mode="inline") gtitle style=daisy;
122 | ods layout gridded columns=3 advance=bygroup;
123 | proc sgplot data=ranked_timings noautolegend ;
124 | hbar name / response=timePerEpisode
125 | categoryorder=respdesc
126 | colorresponse=rank dataskin=crisp datalabelpos=right
127 | datalabel=name datalabelattrs=(size=10pt)
128 | seglabel seglabelattrs=(weight=bold size=10pt color=white) ;
129 | by epLabel notsorted;
130 | format timePerEpisode time.;
131 | label epLabel="Ep";
132 | where rank<=10;
133 | xaxis display=(nolabel) max='00:45:00't min=0 minor grid ;
134 | yaxis display=none grid ;
135 | run;
136 | ods layout end;
137 | ods html5 close;
138 |
139 |
140 | title;
141 | filename all_ep temp;
142 |
143 | ods html5 file=all_ep options(svg_mode="inline") gtitle style=daisy;
144 |
145 | /* Create a summary of scene locations and time spent */
146 | ods graphics / reset width=800 height=600 imagefmt=svg noborder;
147 | proc sgplot data=work.scenes;
148 | hbar location / response=duration
149 | categoryorder=respdesc seglabel seglabelattrs=(color=white weight=bold);
150 | yaxis display=(nolabel);
151 | xaxis label="Scene time (HH:MM:SS)" grid values=(0 to '20:00:00't by '05:00:00't) ;
152 | run;
153 |
154 | /* Break it down for just the Crownlands */
155 | ods graphics / width=500 height=300 imagefmt=svg noborder;
156 | proc sgplot data=work.scenes;
157 | hbar subLocation / response=duration
158 | categoryorder=respdesc seglabel seglabelattrs=(color=white weight=bold);
159 | yaxis display=(nolabel);
160 | xaxis label="Crownlands Scene time (HH:MM:SS)" grid values=(0 to '20:00:00't by '05:00:00't) ;
161 | where location="The Crownlands";
162 | run;
163 |
164 | /* Create a gridded presentation of Episode graphs CUMULATIVE timings */
165 | ods graphics / width=500 height=300 imagefmt=svg noborder;
166 | ods layout gridded columns=3 advance=bygroup;
167 | proc sgplot data=all_times noautolegend ;
168 | hbar name / response=cumulative
169 | categoryorder=respdesc
170 | colorresponse=total_screen_time dataskin=crisp
171 | datalabel=name datalabelpos=right datalabelattrs=(size=10pt)
172 | seglabel seglabelattrs=(weight=bold size=10pt color=white) ;
173 | ;
174 | by epLabel notsorted;
175 | format cumulative time.;
176 | label epLabel="Ep";
177 | where rank<=10;
178 | xaxis display=(nolabel) grid ;
179 | yaxis display=none grid ;
180 | run;
181 | ods layout end;
182 | ods html5 close;
183 |
184 | /* Create a single animated SVG file for all episodes */
185 | options printerpath=svg animate=start animduration=1
186 | svgfadein=.25 svgfadeout=.25 svgfademode=overlap
187 | nodate nonumber;
188 |
189 | /* change this file path to something that works for you */
190 | ODS PRINTER file="c:\temp\got_cumulative.svg" style=daisy;
191 |
192 | /* For SAS University Edition
193 | ODS PRINTER file="/folders/myfolders/got_cumulative.svg" style=daisy;
194 | */
195 |
196 | proc sgplot data=all_times noautolegend ;
197 | hbar name / response=cumulative
198 | categoryorder=respdesc
199 | colorresponse=total_screen_time dataskin=crisp
200 | datalabel=name datalabelpos=right datalabelattrs=(size=10pt)
201 | seglabel seglabelattrs=(weight=bold size=10pt color=white) ;
202 | ;
203 | by epLabel notsorted;
204 | format cumulative time.;
205 | label epLabel="Ep";
206 | where rank<=10;
207 | xaxis label="Cumulative screen time (HH:MM:SS)" grid ;
208 | yaxis display=none grid ;
209 | run;
210 | options animation=stop;
211 | ods printer close;
212 |
--------------------------------------------------------------------------------
/GoT/got_gridded.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sascommunities/sas-dummy-blog/e883da18132d59201195709d81bf8e090e943416/GoT/got_gridded.jpg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/NSDUH/MJ_use_SAMHDA_ALLSTATES.sas:
--------------------------------------------------------------------------------
1 | /* Copyright SAS Institute Inc. */
2 |
3 | /* Create a temp dir to hold the data we download */
4 | options dlcreatedir;
5 | %let workloc = %sysfunc(getoption(WORK))/data;
6 | libname csvloc "&workloc.";
7 | libname csvloc clear;
8 |
9 | /* Collect CSV files for each year */
10 | %macro getStudies;
11 | %do year=2008 %to 2017;
12 | filename study "&workloc./usa_&year..csv";
13 | proc http
14 | method="GET"
15 | url="https://pdas.samhsa.gov/api/surveys/NSDUH-&year.-DS0001/crosstab.csv/?row=CATAG2&column=IRMJRC&weight=ANALWT_C&run_chisq=false"
16 | out=study;
17 | run;
18 | %end;
19 | %mend;
20 |
21 | /* Get annual studies for all years */
22 | %getStudies;
23 |
24 | DATA WORK.MJUse;
25 | LENGTH
26 | fname $ 300
27 | year 8
28 | recency $ 52
29 | age_cat $ 19
30 | Total_percent 8
31 | Total___SE 8
32 | Total___CI__lower_ 8
33 | Total___CI__upper_ 8
34 | Row_percent 8
35 | Row___SE 8
36 | Row___CI__lower_ 8
37 | Row___CI__upper_ 8
38 | Column__ 8
39 | Column___SE 8
40 | Column___CI__lower_ 8
41 | Column___CI__upper_ 8
42 | Weighted_Count 8
43 | Unweighted_Count 8
44 | Count_SE 8 ;
45 | LABEL
46 | recency = "MARIJUANA RECENCY - IMPUTATION REVISED"
47 | age_cat = "Age (recoded)"
48 | Total_percent = "Total %"
49 | Total___SE = "Total % SE"
50 | Total___CI__lower_ = "Total % CI (lower)"
51 | Total___CI__upper_ = "Total % CI (upper)"
52 | Row_percent = "Row %"
53 | Row___SE = "Row % SE"
54 | Row___CI__lower_ = "Row % CI (lower)"
55 | Row___CI__upper_ = "Row % CI (upper)"
56 | Column__ = "Column %"
57 | Column___SE = "Column % SE"
58 | Column___CI__lower_ = "Column % CI (lower)"
59 | Column___CI__upper_ = "Column % CI (upper)"
60 | Weighted_Count = "Weighted Count"
61 | Unweighted_Count = "Unweighted Count"
62 | Count_SE = "Count SE" ;
63 |
64 | INFILE "&workloc./usa_*.csv"
65 | filename=fname
66 | LRECL=32767
67 | FIRSTOBS=2
68 | ENCODING="UTF-8"
69 | DLM='2c'x
70 | MISSOVER
71 | DSD ;
72 | INPUT
73 | recency
74 | age_cat
75 | Total_percent
76 | Total___SE
77 | Total___CI__lower_
78 | Total___CI__upper_
79 | Row_percent
80 | Row___SE
81 | Row___CI__lower_
82 | Row___CI__upper_
83 | Column__
84 | Column___SE
85 | Column___CI__lower_
86 | Column___CI__upper_
87 | Weighted_Count
88 | Unweighted_Count
89 | Count_SE ;
90 |
91 |
92 | year = input(scan(fname,-2,'._'),4.);
93 | if recency ^= "Overall" and age_cat ^= "Overall" and Weighted_count;
94 |
95 | keep year recency age_cat used_12mo
96 | row_percent;
97 | format weighted_count comma12.;
98 |
99 | used_12mo = ( char(recency,1) in ('1','2') );
100 | RUN;
101 |
102 | proc sql;
103 | create table mjsum as
104 | select year, age_cat,
105 | sum(row_percent) as row_percent
106 | from mjuse
107 | where used_12mo=1
108 | group by year, age_cat;
109 | quit;
110 |
111 | ods graphics / width=450 height=600;
112 | /* Use this ODS mod in EG 8.1 to get graph title in image */
113 | /*ods html5(id=eghtml) gtitle;*/
114 | title "Self-reported Marijuana use in past 12 months";
115 | title2 "ALL US STATES, Annual survey";
116 | proc sgplot data=work.mjsum ;
117 | inset "Source:" "National Survey on Drug Use and Health" "samhsa.gov"
118 | / position=topleft border backcolor=white ;
119 | series x=year y=row_percent /
120 | group=age_cat
121 | lineattrs=(thickness=4pt);
122 | xaxis minor values=(2008 to 2017 by 1) display=(nolabel) ;
123 | format
124 | row_percent percent4.1;
125 | yaxis grid display=(nolabel) values=(0 to 1 by 0.05);
126 | keylegend / location=outside position=topleft across=1;
127 | run;
128 |
--------------------------------------------------------------------------------
/NSDUH/MJ_use_SAMHDA_onestate.sas:
--------------------------------------------------------------------------------
1 | /* Copyright SAS Institute Inc. */
2 |
3 | /* Create a temp dir to hold the data we download */
4 | options dlcreatedir;
5 | %let workloc = %sysfunc(getoption(WORK))/data;
6 | libname csvloc "&workloc.";
7 | libname csvloc clear;
8 |
9 | %macro fetchStudy(state=,year=);
10 |
11 | filename study "&workloc./&state._&year..csv";
12 |
13 | /* Marijuana IRMJRC */
14 | /* Alcohol: IRALCRC */
15 | /* Cigarettes: IRCIGRC */
16 | proc http
17 | method="GET"
18 | url="https://rdas.samhsa.gov/api/surveys/NSDUH-&year.-RD02YR/crosstab.csv/?row=CATAG2%str(&)column=IRMJRC%str(&)control=STNAME%str(&)weight=DASWT_1%str(&)run_chisq=false%str(&)filter=STNAME%nrstr(%3D)&state."
19 | out=study;
20 | run;
21 |
22 | %mend;
23 |
24 | %let state=COLORADO;
25 |
26 | /* Download data for each 2-year study period */
27 | %fetchStudy(state=&state., year=2016-2017);
28 | %fetchStudy(state=&state., year=2015-2016);
29 | %fetchStudy(state=&state., year=2014-2015);
30 | %fetchStudy(state=&state., year=2012-2013);
31 | %fetchStudy(state=&state., year=2010-2011);
32 | %fetchStudy(state=&state., year=2008-2009);
33 | %fetchStudy(state=&state., year=2006-2007);
34 |
35 |
36 | DATA WORK.MJState;
37 | LENGTH
38 | year 8
39 | fname $ 300
40 | STATE_NAME $ 15
41 | recency $ 52
42 | age_cat $ 19
43 | Total_pc 8
44 | Total_pcSE 8
45 | Total_pcCI__lower_ 8
46 | Total_pcCI__upper_ 8
47 | row_percent 8
48 | Row_pcSE 8
49 | Row_pcCI_lower 8
50 | Row_pcCI_upper 8
51 | Column_pc 8
52 | Column_pcSE 8
53 | Column_pcCI__lower_ 8
54 | Column_pcCI__upper_ 8
55 | Weighted_Count 8
56 | Count_SE 8;
57 | LABEL
58 | recency = "drug use recency, imputed revised"
59 | age_cat = "AGE (recoded)"
60 | ;
61 | INFILE "&workloc./&state._*.csv"
62 | filename=fname
63 | LRECL=32767
64 | FIRSTOBS=2
65 | ENCODING="UTF-8"
66 | DLM='2c'x
67 | MISSOVER
68 | DSD;
69 | INPUT
70 | state_name
71 | recency
72 | age_cat
73 | Total_pc
74 | Total_pcSE
75 | Total_pcCI__lower_
76 | Total_pcCI__upper_
77 | row_percent
78 | Row_pcSE
79 | Row_pcCI_lower
80 | Row_pcCI_upper
81 | Column_pc
82 | Column_pcSE
83 | Column_pcCI__lower_
84 | Column_pcCI__upper_
85 | Weighted_Count
86 | Count_SE;
87 |
88 | /* get year from filename */
89 | year = input( scan(fname,-2,'._-'), 4.);
90 |
91 | /* trim out the summarized lines/columns */
92 | if state_name="&state." and age_cat ^= "Overall" and recency ^="Overall";
93 | keep recency age_cat row_percent year
94 | used_12mo Row_pcCI_lower Row_pcCI_upper;
95 |
96 | /* recoded recency to 12mo, yes or no */
97 | used_12mo = ( char(recency,1) in ('1','2') );
98 |
99 | /* Trim ordinal indicator from age category */
100 | age_cat = substr(age_cat,4);
101 | RUN;
102 |
103 | /* Summarize to the "used in past 12 mo" category */
104 | proc sql;
105 | create table mjsum as
106 | select year, age_cat,
107 | sum(row_percent) as row_percent
108 | from mjState
109 | where used_12mo=1
110 | group by year, age_cat;
111 | quit;
112 |
113 | ods graphics / width=450 height=600;
114 |
115 | /* Use this ODS mod in EG 8.1 to include title in graph file */
116 | /*ods html5(id=eghtml) gtitle;*/
117 |
118 | title "Self-reported Marijuana use in past 12 months";
119 | title2 "&state., 2-year study survey results";
120 | proc sgplot data=work.mjsum ;
121 | inset "Source:" "National Survey on Drug Use and Health" "samhsa.gov"
122 | / position=topleft border backcolor=white ;
123 | series x=year y=row_percent /
124 | group=age_cat
125 | markers markerattrs=(size=12 symbol=Diamond color=blue)
126 | lineattrs=(thickness=4pt);
127 | xaxis minor grid display=(nolabel)
128 | values=(2007 to 2017 by 1) ;
129 | format
130 | row_percent percent4.1;
131 |
132 | yaxis grid display=(nolabel) min=0 max=1
133 | values=(0 to 1 by .05);
134 | keylegend / location=outside position=topleft across=1;
135 |
136 | run;
137 |
138 | /* An example of a Band plot to show Confidence Interval */
139 | title "Use in just past 30 days, with CI";
140 | proc sgplot data=WORK.MJState;
141 | band x=year upper=Row_pcCI_upper lower=Row_pcCI_lower
142 | / group=age_cat transparency=0.4;
143 | series x=year y=row_percent / group=age_cat lineattrs=(thickness=3);
144 | yaxis display=(nolabel) values=(0 to 1 by .1);
145 | format row_percent percent6.;
146 | where recency ? "past 30 days";
147 | run;
148 |
--------------------------------------------------------------------------------
/NSDUH/README.md:
--------------------------------------------------------------------------------
1 | # A skeptic's guide to statistics in the media
2 |
3 | This repository contains code files for the blog post on
4 | [The SAS Dummy blog](https://blogs.sas.com/content/sasdummy/2019/08/19/media-data-literacy/).
5 |
6 | The code collects data from the published surveys at [SAMHSA.gov](https://samhsa.gov/data/),
7 | in particular the [National Survey on Drug Use and Health (NSDUH)](https://www.samhsa.gov/data/data-we-collect/nsduh-national-survey-drug-use-and-health).
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The SAS Dummy blog
2 |
3 | This repository contains code files for many of the examples that you'll find on [The SAS Dummy blog](https://blogs.sas.com/sasdummy).
4 |
5 | ## Support
6 | These code files are provided as examples and as such aren't officially supported by SAS. However, I encourage you to visit the blog and ask questions in the comments. I respond to all comments, answer questions when I can, and make corrections when readers point out mistakes. (Yes, it happens.)
7 |
--------------------------------------------------------------------------------
/census-2020/state-apportionment-2020-sgmap.sas:
--------------------------------------------------------------------------------
1 | /* Adapted from Robert Allison's blog post 30-Apr2021 */
2 | /* https://blogs.sas.com/content/graphicallyspeaking/feeling-the-effects-of-the-2020-census/ */
3 | /* Adjusted to run well in SAS Studio (SAS OnDemand for Academics, SAS University Edition, etc) */
4 |
5 | /*
6 | Using Census data from:
7 | https://www.census.gov/data/tables/2020/dec/2020-apportionment-data.html
8 | Specifically:
9 | https://www2.census.gov/programs-surveys/decennial/2020/data/apportionment/apportionment-2020-table01.xlsx
10 | */
11 |
12 | /* Use PROC HTTP to fetch data dynamically */
13 | filename census temp;
14 |
15 | proc http
16 | url="https://www2.census.gov/programs-surveys/decennial/2020/data/apportionment/apportionment-2020-table01.xlsx"
17 | out=census;
18 | run;
19 |
20 | /* Force PROC IMPORT to use V7 (traditional) rules for var names */
21 | options validvarname=v7;
22 |
23 | /* Import the census data */
24 | /* pull from a specific range, rename variables in output */
25 | proc import file=census out=my_data
26 | (rename=(state=state_name
27 | var2=apport_population
28 | NUMBER_OF_APPORTIONED_REPRESENTA=reps
29 | CHANGE_FROM___2010_CENSUS_APPORT=change_reps))
30 | dbms=xlsx replace;
31 | getnames=yes;
32 | range='Table 1$A4:D54';
33 | run;
34 |
35 | /* Merge in the 2-character state code for each state_name */
36 | proc sql noprint;
37 | create table withcodes as select unique my_data.*, us_data.statecode from
38 | my_data left join sashelp.us_data on my_data.state_name=us_data.statename;
39 | quit;
40 |
41 | run;
42 |
43 | /* Create dataset of the labels to overlay */
44 | data my_labels;
45 | set withcodes (where=(change_reps^=0));
46 | length change_reps_text $10;
47 | if change_reps>0 then
48 | change_reps_text='+'||trim(left(change_reps));
49 | else
50 | change_reps_text=trim(left(change_reps));
51 | run;
52 |
53 | /* Get the projected x/y centroid, that match up with the mapsgfk.us */
54 | proc sql noprint;
55 | create table centroids as select unique my_labels.*, uscenter.x, uscenter.y
56 | from my_labels left join mapsgfk.uscenter on
57 | my_labels.statecode=uscenter.statecode;
58 | quit;
59 |
60 | run;
61 |
62 | /* get the map polygons, excluding DC */
63 | data my_map;
64 | set mapsgfk.us (where=(statecode^='DC'));
65 | run;
66 |
67 | /* sort the data (this can affect the order of the colors) */
68 | proc sort data=my_data out=my_data;
69 | by change_reps;
70 | run;
71 |
72 | ods html5(id=web) gtitle gfootnote;
73 | ods graphics /
74 | noscale /* if you don't use this option, the text will be resized */
75 | imagemap tipmax=2500 width=900px height=600px;
76 |
77 | title1 color=gray33 height=20pt
78 | "State Apportionment Changes based on 2020 Census";
79 | footnote color=cornflowerblue height=12pt
80 | "Source: https://www.census.gov/data/tables/2020/dec/2020-apportionment-data.html";
81 |
82 | proc sgmap maprespdata=withcodes mapdata=my_map plotdata=centroids noautolegend;
83 | styleattrs datacolors=(white cxfdbf6f cxb2df8a cx33a02c);
84 | choromap change_reps / discrete mapid=statecode lineattrs=(thickness=1
85 | color=gray88) tip=(state_name reps change_reps);
86 | text x=x y=y text=change_reps_text / position=center textattrs=(color=gray33
87 | size=14pt weight=bold) tip=none;
88 | run;
--------------------------------------------------------------------------------
/currency/currency.sas:
--------------------------------------------------------------------------------
1 | /*
2 | filename data url "https://www.federalreserve.gov/paymentsystems/files/coin_currcircvolume.txt";
3 | */
4 |
5 | filename data temp;
6 | proc http
7 | url="https://www.federalreserve.gov/paymentsystems/files/coin_currcircvolume.txt"
8 | method="GET"
9 | out=data;
10 | run;
11 |
12 | data fromfed;
13 | length
14 | year 8
15 | notes_1 8
16 | notes_2 8
17 | notes_5 8
18 | notes_10 8
19 | notes_20 8
20 | notes_50 8
21 | notes_100 8
22 | notes_500plus 8 ;
23 | infile data
24 | firstobs=5 dlm='09'x
25 | encoding="utf-8"
26 | truncover ;
27 | input year notes_1 notes_2 notes_5 notes_10 notes_20 notes_50 notes_100 notes_500plus;
28 | if year ^= .;
29 | run;
30 |
31 | /* Add and ID value to prepare for transpose */
32 | proc sql ;
33 | create table circdata as select t1.*,
34 | 'BillCountsInBillions' as ID from fromfed t1
35 | order by year;
36 | run;
37 |
38 | /* Stack this data into column-wise format */
39 | proc transpose data = work.circdata
40 | out=work.stackedcash
41 | name=denomination
42 | label=countsinbillions
43 | ;
44 | by year;
45 | id id;
46 | var notes_1 notes_2 notes_5 notes_10 notes_20 notes_50 notes_100 notes_500plus;
47 | run;
48 |
49 |
50 | /* Calculate the dollar values based on counts */
51 | data cashvalues;
52 | set stackedcash;
53 | length multiplier 8 value 8;
54 | select (denomination);
55 | when ('notes_1') multiplier=1;
56 | when ('notes_2') multiplier=2;
57 | when ('notes_5') multiplier=5;
58 | when ('notes_10') multiplier=10;
59 | when ('notes_20') multiplier=20;
60 | when ('notes_50') multiplier=50;
61 | when ('notes_100') multiplier=100;
62 | when ('notes_500plus') multiplier=500;
63 | otherwise multiplier=0;
64 | end;
65 | value = BillCountsInBillions * multiplier;
66 | run;
67 |
68 | /* Use a format to make a friendlier legend in our plots */
69 | proc format lib=work;
70 | value $notes
71 | "notes_1" = "$1"
72 | "notes_2" = "$2"
73 | "notes_5" = "$5"
74 | "notes_10" = "$10"
75 | "notes_20" = "$20"
76 | "notes_50" = "$50"
77 | "notes_100" = "$100"
78 | "notes_500plus" = "$500+"
79 | ;
80 | run;
81 |
82 | proc freq data=cashvalues
83 | order=data
84 | noprint
85 | ;
86 | tables denomination / nocum out=work.cashpercents scores=table;
87 | weight value;
88 | by year;
89 | run;
90 |
91 | proc freq data=cashvalues
92 | order=data
93 | noprint
94 | ;
95 | tables denomination / nocum out=work.billpercents scores=table;
96 | weight BillCountsInBillions;
97 | by year;
98 | run;
99 |
100 | /* directives for EG HTML output */
101 | ods html5 (id=eghtml) gtitle gfootnote;
102 | ods graphics / width=700px height=400px;
103 |
104 | /* Plot the results */
105 | footnote height=1 'Source: https://www.federalreserve.gov/paymentsystems/coin_currcircvolume.htm';
106 | title "US Currency in Circulation: % Value of Denominations";
107 | proc sgplot data=cashpercents ;
108 | label denomination = 'Denomination';
109 | format denomination $notes.;
110 | vbar year / response=percent group=denomination grouporder=data;
111 | yaxis label="% Value in Billions" ;
112 | xaxis display=(nolabel);
113 | keylegend / position=right across=1 noborder valueattrs=(size=12pt) title="" ;
114 | run;
115 |
116 | title "US Currency in Circulation: % Bill Counts";
117 | proc sgplot data=billpercents ;
118 | label denomination = 'Denomination';
119 | format denomination $notes.;
120 | vbar year / response=percent group=denomination grouporder=data;
121 | yaxis label="% Bills in Billions" ;
122 | xaxis display=(nolabel);
123 | keylegend / position=right across=1 noborder valueattrs=(size=12pt) title="";
124 | run;
125 |
126 | title "US Currency in Circulation: Total Value of Denominations";
127 | proc sgplot data=cashvalues ;
128 | label denomination = 'Denomination';
129 | format denomination $notes.;
130 | vbar year / response=value group=denomination grouporder=data ;
131 | yaxis label="Value in Billions" ;
132 | xaxis display=(nolabel);
133 | keylegend / position=right across=1 noborder valueattrs=(size=12pt) title="";
134 | run;
135 |
136 | title "US Currency in Circulation: Bill Counts";
137 | proc sgplot data=cashvalues;
138 | label denomination = 'Denomination';
139 | format denomination $notes.;
140 | vbar year / response=BillCountsInBillions group=denomination grouporder=data;
141 | keylegend / position=right across=1 noborder valueattrs=(size=12pt) title="";
142 | yaxis label="Bill Counts in Billions";
143 | xaxis display=(nolabel);
144 | run;
--------------------------------------------------------------------------------
/emojis/emoji-sequences.sas:
--------------------------------------------------------------------------------
1 | /*
2 | Sample code for pulling emoji data into SAS
3 | Code by: Chris Hemedinger, SAS
4 |
5 | NOTE: These data require ENCODING=UTF8 in your SAS session!
6 |
7 | Pull emoji definitions from Unicode.org
8 | Each version of the emoji standard has a data file.
9 | This code pulls from the "latest" to get the most current set.
10 | */
11 |
12 | filename raw temp;
13 | proc http
14 | url="https://unicode.org/Public/emoji/latest/emoji-sequences.txt"
15 | out=raw;
16 | run;
17 |
18 | ods escapechar='~';
19 | data emojis (drop=line);
20 | length line $ 1000 codepoint_range $ 45 val_start 8 val_end 8
21 | type $ 30 comments $ 65 saschar $ 20 htmlchar $ 25;
22 | infile raw ;
23 | input;
24 | line = _infile_;
25 |
26 | /* skip comments and blank lines */
27 | /* data fields are separated by semicolons */
28 | if substr(line,1,1)^='#' and line ^= ' ' then do;
29 |
30 | /* read the raw codepoint value - could be single, a range, or a combo of several */
31 | codepoint_range = scan(line,1,';');
32 | /* read the type field */
33 | type = compress(scan(line,2,';'));
34 | /* text description of this emoji */
35 | comments = scan(line,3,'#;');
36 |
37 | /* for those emojis that have a range of values */
38 | val_start = input(scan(codepoint_range,1,'. '), hex.);
39 | if find(codepoint_range,'..') > 0 then do;
40 | val_end = input(scan(codepoint_range,2,'.'), hex.);
41 | end;
42 | else val_end=val_start;
43 |
44 | if type = "Basic_Emoji" then do;
45 | saschar = cat('~{Unicode ',scan(codepoint_range,1,' .'),'}');
46 | htmlchar = cats('',scan(codepoint_range,1,' .'),';');
47 | end;
48 | output;
49 | end;
50 | run;
51 |
52 | /* Assuming HTML or HTML5 output destination */
53 | /* print the first 50 emoji records */
54 | proc print data=emojis (obs=50); run;
55 |
56 |
57 | /*
58 | Instead of the Unicode.org data, let's pull data
59 | from a structured data file built for another
60 | project on GitHub -- the gemoji project
61 |
62 | */
63 |
64 | filename rawj temp;
65 | proc http
66 | url="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
67 | out=rawj;
68 | run;
69 |
70 | libname emoji json fileref=rawj;
71 |
72 | /* reformat the tags and aliases data for inclusion in a single data set */
73 | data tags;
74 | length ordinal_root 8 tags $ 60;
75 | set emoji.tags;
76 | tags = catx(', ',of tags:);
77 | keep ordinal_root tags;
78 | run;
79 |
80 | data aliases;
81 | length ordinal_root 8 aliases $ 60;
82 | set emoji.aliases;
83 | aliases = catx(', ',of aliases:);
84 | keep ordinal_root aliases;
85 | run;
86 |
87 | /* Join together in one record per emoji */
88 | proc sql;
89 | create table full_emoji as
90 | select t1.emoji as emoji_char,
91 | unicodec(t1.emoji,'esc') as emoji_code,
92 | t1.description, t1.category, t1.unicode_version,
93 | case
94 | when t1.skin_tones = 1 then t1.skin_tones
95 | else 0
96 | end as has_skin_tones,
97 | t2.tags, t3.aliases
98 | from emoji.root t1
99 | left join tags t2 on (t1.ordinal_root = t2.ordinal_root)
100 | left join aliases t3 on (t1.ordinal_root = t3.ordinal_root)
101 | ;
102 | quit;
103 |
104 | /* Assuming HTML or HTML5 output destination */
105 | proc print data=full_emoji; run;
106 |
107 |
108 |
--------------------------------------------------------------------------------
/import-xls/np_info.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sascommunities/sas-dummy-blog/e883da18132d59201195709d81bf8e090e943416/import-xls/np_info.xlsx
--------------------------------------------------------------------------------
/includesasnb/README.md:
--------------------------------------------------------------------------------
1 | # %includesasnb -- read in and run code from a SAS Notebook
2 |
3 | *Purpose:* Extract the code cells from a SAS Notebook file (.sasnb) and submit as a %include action in SAS.
4 |
5 | This macro reads just the code-style cells from the SAS Notebook file and assembles them into one continuous SAS program. It then uses %INCLUDE to bring that program into your current SAS session and run it.
6 |
7 | **NOTE**: This macro does *not* update the content of the SAS Notebook file with any SAS log or other results.
8 |
9 | Currently, SAS Notebook files support these type of code cells: SAS, SQL and Python. For SQL and Python, this macro adds the required PROC SQL/QUIT or PROC PYTHON/SUBMIT/ENDSUBMIT/RUN sections so the complete code runs in SAS.
10 |
11 | The **filepath**= argument tells the macro where to find the SAS Notebook file. There are also two optional arguments:
12 |
13 | - **outpath=** Path to output file to save the code as a persistent .SAS file. If you do not specify outpath=, the code is stored in a temp location and deleted at the end of your SAS session.
14 | - **noexec=** 1 | 0 - If 1, then the code is not run but just echoed to the SAS log or output to the outpath= location.
15 |
16 | ### Example use
17 | ```
18 | /* Build SAS code from code cells and run in current session */
19 |
20 | %includesasnb(filepath=C:\Projects\sas-microsoft365-example.sasnb);
21 |
22 | /* To echo code to log and NOT run it: */
23 |
24 | %includesasnb(filepath=C:\Projects\sas-microsoft365-example.sasnb,noexec=1);
25 |
26 | /* To save the notebook code in persistent SAS file AND run it: */
27 |
28 | %includesasnb(filepath=C:\Projects\sas-microsoft365-example.sasnb,
29 | outfile=c:\projects\newfile.sas);
30 |
31 | ```
32 |
33 | ### Limitations
34 |
35 | - There is a 32K byte limit on each code cell that is read. Since these are read into SAS variables before written to a file, any code block larger than 32K will be truncated. The total amount of code cells don't have a limit; the 32K limit applies just to a single cell.
36 |
37 | - The .sasnb file must be on the file system. A future enhancement might add ability to read from a SAS Content folder in SAS Viya 4; let me know if that would be useful for you.
--------------------------------------------------------------------------------
/includesasnb/includesasnb.sas:
--------------------------------------------------------------------------------
1 | /*-----------------------------------------------------------------------------------*/
2 | /* Extract the code cells from a SAS Notebook file (.sasnb) and submit as a %include */
3 | /* filepath = */
4 | /* outpath = OPTIONAL: */
5 | /* noexec = 1 | 0 OPTIONAL: 1 is default, and code will be run. */
6 | /* If 0 will just output code to log or outfile if specified */
7 | /* */
8 | /* Example use: */
9 | /* %includesasnb(filepath=C:\Projects\sas-microsoft365-example.sasnb); */
10 | /* To echo code to log and NOT run it: */
11 | /* %includesasnb(filepath=C:\Projects\sas-microsoft365-example.sasnb,noexec=1); */
12 | /* To save the notebook code in persistent SAS file: */
13 | /* %includesasnb(filepath=C:\Projects\sas-microsoft365-example.sasnb, */
14 | /* outfile=c:\projects\newfile.sas); */
15 | /*-----------------------------------------------------------------------------------*/
16 | %macro includesasnb(
17 | filepath=, /* REQUIRED: full path to the .sasnb file */
18 | outpath=, /* OPTIONAL: persistent location for .SAS file to save .sasnb as program */
19 | noexec=0 /* OPTIONAL: whether to skip execution of code or just echo to log or file */
20 | );
21 | filename _nb "&filepath.";
22 | %if %sysfunc(fexist(_nb)) %then
23 | %do;
24 | libname _nb JSON fileref=_nb;
25 |
26 | %if "&outpath." ^= "" %then %do;
27 | filename _code "&outpath." ;
28 | %end;
29 | %else %do;
30 | filename _code temp;
31 | %end;
32 |
33 | /* Pull just the code cells from the notebook file */
34 | /* SQL and Python code must be wrapped in step syntax */
35 | data _null_;
36 | set _nb.root;
37 | file _code;
38 | if _n_ = 1 then do;
39 | put "/* Code generated from &filepath. */";
40 | end;
41 |
42 | if language="sas" then
43 | do;
44 | put value;
45 | end;
46 |
47 | if language="sql" then
48 | do;
49 | put "proc sql;";
50 | put value;
51 | put "quit;";
52 | end;
53 |
54 | if language="python" then
55 | do;
56 | put "proc python;";
57 | put "submit;";
58 | put value;
59 | put "endsubmit;";
60 | put "run;";
61 | end;
62 | run;
63 | %if &noexec = 0 %then
64 | %do;
65 | %include _code;
66 | %end;
67 | %else
68 | %do;
69 | %if "&outpath." = "" %then
70 | %do;
71 | /* echo to log only */
72 | %put --------------------------------;
73 | %put Code cells from &filepath.;
74 | %put --------------------------------;
75 |
76 | data _null_;
77 | infile _code;
78 | file log;
79 | input;
80 | put _infile_;
81 | run;
82 | %end;
83 | %end;
84 | libname _nb clear;
85 | filename _code clear;
86 | %end;
87 | %else %put ERROR: File &filepath. does not exist;
88 | filename _nb clear;
89 | %mend;
90 |
91 |
--------------------------------------------------------------------------------
/leadingzeros/leadingzeros.sas:
--------------------------------------------------------------------------------
1 | data customer;
2 | infile datalines dlm="|";
3 | input CustomerID: 8. Zip: 8. City:$25. Income: 8. CreditScore: 8. Value 8.;
4 | datalines4;
5 | 1928446490 |55901 |Rochester |6833.58 |596 | 67.04
6 | 1900730536 |07010 |Cliffside Park |. |746 | 6.02
7 | 1960231260 |07203 |Roselle |64183.78 |585 | 59.12
8 | 1995545096 |07087 |Union City |65974.21 |662 | .38
9 | 1943535328 |01850 |Lowell |. |657 | .26
10 | 1989152037 |01752 |Marlborough |63857.83 |660 | 11.2
11 | 1989638715 |31901 |Columbus |. |682 | 4.78
12 | 1953531885 |30042 |Lawrenceville |47999.48 |711 | 31.82
13 | 1908378591 |60502 |Aurora |58486 |695 | 41.11
14 | 1944290137 |01850 |Lowell |. |689 | .79
15 | 1908316871 |52627 |Fort Madison |72123.43 |644 | .2333
16 | 1987648131 |47701 |Evansville |. |667 | 1.8
17 | 1941732136 |47801 |Terre Haute |59351.19 |. | 61.1
18 | 1993690772 |71291 |West Monroe |. |616 | 3.7888
19 | 1963535911 |07101 |Newark |52353.55 |676 | 25.062
20 | ;;;;
21 | run;
22 |
23 |
24 | /*Documentation - https://go.documentation.sas.com/?docsetId=ds2ref&docsetTarget=p1h8l8v2o11xhnn1oue05oue1hvx.htm&docsetVersion=3.1&locale=en*/
25 | /*Z Format - Writes standard numeric data with leading 0s.*/
26 |
--------------------------------------------------------------------------------
/misc/encodedheart.sas:
--------------------------------------------------------------------------------
1 | /* BinaryHeart adapted from Rick Wicklin's blog */
2 | /* ODS statements assume you're running in */
3 | /* SAS Enterprise Guide with HTML destination */
4 | /* Shared on "Social Media Love" day */
5 | /* https://www.instagram.com/p/BzVo5CuHtSn/ */
6 | data BinaryHeart;
7 | drop Nx Ny t r;
8 | Nx = 21; Ny = 23;
9 | call streaminit(2142015);
10 | do x = -2.6 to 2.6 by 5.2/(Nx-1);
11 | do y = -4.4 to 1.5 by 6/(Ny-1);
12 | r = sqrt( x**2 + y**2 );
13 | t = atan2(y,x);
14 | Heart=(r < 2 - 2*sin(t) + sin(t)*sqrt(abs(cos(t))) /
15 | (sin(t)+1.4))
16 | & (y > -3.5);
17 | B = rand("Bernoulli", 0.5);
18 | output;
19 | end;
20 | end;
21 | run;
22 | ods html5(eghtml) gtitle style=raven;
23 | ods graphics / width=550px height=550px;
24 | title height=2.5 "You're encoded in our hearts";
25 | proc sgplot data=BinaryHeart noautolegend;
26 | styleattrs datacontrastcolors=(lightgray red);
27 | scatter x=x y=y / group=Heart markerchar=B
28 | markercharattrs=(size=14);
29 | xaxis display=none offsetmin=0 offsetmax=0.06;
30 | yaxis display=none;
31 | run;
--------------------------------------------------------------------------------
/msteams/README.md:
--------------------------------------------------------------------------------
1 | # Publish to Microsoft Teams with SAS
2 |
3 | This repository contains code files for the blog post on
4 | [The SAS Dummy blog](https://blogs.sas.com/content/sasdummy/sas-microsoft-teams/).
5 |
6 | To provide a rich, integrated experience with Microsoft Teams, you can publish content using a webhook. A webhook is a REST API endpoint that allows you to post messages and notifications with more control over the appearance and interactive options within the messages. In SAS, you can publish to a webhook by using PROC HTTP.
7 |
--------------------------------------------------------------------------------
/msteams/webhook_publish.sas:
--------------------------------------------------------------------------------
1 | /* Copyright 2019 SAS Institute Inc. */
2 |
3 | /* Create dummy JSON to represent the recommendation engine */
4 | /* API response */
5 |
6 | filename apiout temp;
7 | data _null_;
8 | file apiout;
9 | infile datalines;
10 | input;
11 | put _infile_;
12 | datalines4;
13 | {
14 | "anonymous_astore_creation": "05Sep2019:13:05:00",
15 | "astore_creation": "05Sep2019:13:30:06",
16 | "conversation_uid": "[250217,20045,216873,196443,251360]",
17 | "cooloff_records": 65239,
18 | "cooloff_users": 29540,
19 | "creation": "Thu Sep 5 13:30:41 2019",
20 | "num_topics": 78378,
21 | "num_users": 148686,
22 | "personalized": true,
23 | "process_time": "0.3103 seconds"
24 | }
25 | ;;;;
26 | run;
27 |
28 | /* Create the first part of the boilerplate for the message card */
29 | filename heading temp;
30 | data _null_;
31 | ts = cat('"activitySubtitle": "As of ',"%trim(%sysfunc(datetime(),datetime20.))",'",');
32 | infile datalines4;
33 | file heading;
34 | input;
35 | if (find(_infile_,"TIMESTAMP")>0)
36 | then put ts;
37 | else put _infile_;
38 | datalines4;
39 | {
40 | "@type": "MessageCard",
41 | "@context": "https://schema.org/extensions",
42 | "summary": "Recommendation Engine Health Check",
43 | "themeColor": "0075FF",
44 | "sections": [
45 | {
46 | "startGroup": true,
47 | "title": "**Recommendation Engine Heartbeat**",
48 | "activityImage": "",
49 | "activityTitle": "**PRODUCTION** endpoint check",
50 | TIMESTAMP
51 | "facts":
52 | ;;;;
53 | run;
54 |
55 | /* Read the "API response" */
56 | libname prod json fileref=apiout;
57 |
58 | data segment (keep=name value);
59 | set prod.root;
60 | name="Score data updated (UTC)";
61 | value= astore_creation;
62 | output;
63 | name="Topics scored";
64 | value=left(num_topics);
65 | output;
66 | name="Number of users";
67 | value= left(num_users);
68 | output;
69 | name="Process time";
70 | value= process_time;
71 | output;
72 | run;
73 |
74 | /* generate the "Facts" segment */
75 | filename segment temp;
76 | proc json out=segment nosastags pretty;
77 | export segment;
78 | run;
79 |
80 | /* Combine it all together */
81 | filename msg temp;
82 | data _null_;
83 | file msg;
84 | infile heading end=eof;
85 | do while (not eof);
86 | input;
87 | put _infile_;
88 | end;
89 | infile segment end=eof2;
90 | do while (not eof2);
91 | input;
92 | put _infile_;
93 | if eof2 then put "} ] }";
94 | end;
95 | run;
96 |
97 | /* Publish to Teams channel with a webhook */
98 | proc http
99 | method="POST"
100 | ct="text/plain"
101 | url="https://outlook.office.com/webhook/my-unique-webhook-endpoint"
102 | in=msg;
103 | run;
104 |
--------------------------------------------------------------------------------
/netflix-trip/NetflixTrip.sas:
--------------------------------------------------------------------------------
1 | /* Utility macro to check if folder is empty before Git clone */
2 | %macro FolderIsEmpty(folder);
3 | %local memcount;
4 | %let memcount = 0;
5 | %let filrf=mydir;
6 | %let rc=%sysfunc(filename(filrf, "&folder."));
7 | %if &rc. = 0 %then %do;
8 | %let did=%sysfunc(dopen(&filrf));
9 | %let memcount=%sysfunc(dnum(&did));
10 | %let rc=%sysfunc(dclose(&did));
11 | %let rc=%sysfunc(filename(filrf));
12 | %end;
13 | /* Value to return: 1 if empty, else 0 */
14 | %sysevalf(&memcount. eq 0)
15 | %mend;
16 |
17 | options dlcreatedir;
18 | %let repopath=%sysfunc(getoption(WORK))/sas-netflix-git;
19 | libname repo "&repopath.";
20 | data _null_;
21 | if (%FolderIsEmpty(&repoPath.)) then do;
22 | rc = gitfn_clone(
23 | "https://github.com/cjdinger/sas-netflix-git",
24 | "&repoPath."
25 | );
26 | put 'Git repo cloned ' rc=;
27 | end;
28 | else put "Skipped Git clone, folder not empty";
29 | run;
30 |
31 | filename viewing "&repopath./NetflixData/*.csv";
32 | data viewing (keep=title date profile maintitle episode season);
33 | length title $ 300 date 8
34 | maintitle $ 60 episode $ 40 season $ 12
35 | profile $ 40 in $ 250;
36 | format date date9.;
37 | infile viewing dlm=',' dsd filename=in firstobs=2;
38 | profile=scan(in,-1,'\/');
39 | input title date:??mmddyy.;
40 | if date^=. and title ^="";
41 | array part $ 60 part1-part4;
42 | do i = 1 to 4;
43 | part{i} = scan(title, i, ':',);
44 | if (find(part{i},"Season")>0)
45 | then do;
46 | season=part{i};
47 | end;
48 | end;
49 | drop i;
50 | maintitle = part{1};
51 | episode = part{3};
52 | run;
53 |
54 | PROC SQL noprint;
55 | CREATE TABLE WORK.Office AS
56 | SELECT t1.date,
57 | (COUNT(t1.date)) AS Episodes
58 | FROM WORK.VIEWING t1
59 | WHERE t1.maintitle = 'The Office (U.S.)'
60 | GROUP BY t1.date ;
61 | %let days = &sqlobs;
62 |
63 | select sum(episodes) into:episodes trimmed from office;
64 | QUIT;
65 |
66 | data dates;
67 | length date 8 year 8 day 8 month 8 monyear 8 ;
68 | format date date9. monyear yymmd7.;
69 | do date='01oct2017'd to '01jan2021'd;
70 | year=year(date);
71 | day = day(date);
72 | month=month(date);
73 | monyear = intnx('month',date,0,'b');
74 | output;
75 | end;
76 | run;
77 |
78 | proc sql noprint;
79 | create table ofc_viewing as
80 | select t1.*,
81 | case
82 | when t2.Episodes not is missing then t2.Episodes
83 | else 0
84 | end as Episodes
85 | from dates t1 left join office t2
86 | on (t1.date=t2.date)
87 | ;
88 |
89 | select distinct monyear format=6. into: allmon separated by ' ' from ofc_viewing;
90 | quit;
91 |
92 | /* for use in SAS Enterprise Guide to overide styles */
93 | ods html5(id=eghtml) gtitle gfootnote style=raven;
94 |
95 | ods graphics / width=1100 height=1000 ;
96 | proc sgplot data=ofc_viewing;
97 | title height=2.5 "The Office - a Netflix Journey";
98 | title2 height=2 "&episodes. episodes streamed on &days. days, over 3 years";
99 | label Episodes="Episodes per day";
100 | format monyear monyy7.;
101 | heatmapparm x=day y=monyear
102 | colorresponse=episodes / x2axis
103 | outline
104 | colormodel=(white CXfcae91 CXfb6a4a CXde2d26 CXa50f15) ;
105 | yaxis minor reverse display=(nolabel)
106 | values=(&allmon.)
107 | ;
108 | x2axis values=(1 to 31 by 1)
109 | display=(nolabel) ;
110 | run;
111 |
112 | title "Frequency of Episodes per Season";
113 | ods graphics / height=400 width=800;
114 | proc freq data=viewing order=formatted;
115 | where maintitle = 'The Office (U.S.)';
116 | table season / plots=freqplot;
117 | run;
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/onedrive/README.md:
--------------------------------------------------------------------------------
1 | # Using SAS to access and update files on Microsoft OneDrive
2 |
3 | These files support [the article published here](https://blogs.sas.com/content/sasdummy/sas-programming-office-365-onedrive/).
4 |
5 | Updated on 21-Oct-2019 with a new example for Sharepoint Online. See [onedrive_sharepoint_example.sas](./onedrive_sharepoint_example.sas).
6 |
7 | Questions or problems? Visit the blog and post a comment.
8 |
--------------------------------------------------------------------------------
/onedrive/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "tenant_id": "YOUR TENANT ID HERE",
3 | "client_id": "YOUR APPLICATION (CLIENT) ID HERE",
4 | "redirect_uri": "https://login.microsoftonline.com/common/oauth2/nativeclient",
5 | "resource" : "https://graph.microsoft.com"
6 | }
7 |
--------------------------------------------------------------------------------
/onedrive/onedrive_config.sas:
--------------------------------------------------------------------------------
1 | /*
2 | Set the variables that will be needed through the code
3 | We'll need these for authorization and also for runtime
4 | use of the service.
5 |
6 | Reading these from a config.json file so that the values
7 | are easy to adapt for different users or projects.
8 | */
9 |
10 | %if %symexist(config_root) %then %do;
11 | filename config "&config_root./config.json";
12 | libname config json fileref=config;
13 | data _null_;
14 | set config.root;
15 | call symputx('tenant_id',tenant_id,'G');
16 | call symputx('client_id',client_id,'G');
17 | call symputx('redirect_uri',redirect_uri,'G');
18 | call symputx('resource',resource,'G');
19 | run;
20 | %end;
21 | %else %do;
22 | %put ERROR: You must define the CONFIG_ROOT macro variable.;
23 | %end;
--------------------------------------------------------------------------------
/onedrive/onedrive_example_use.sas:
--------------------------------------------------------------------------------
1 | /* ----------------------------------------------------
2 | Example API calls from SAS to
3 | Microsoft Office 365 OneDrive.
4 |
5 | Authors: Joseph Henry, SAS
6 | Chris Hemedinger, SAS
7 | Copyright 2018, SAS Institute Inc.
8 | -------------------------------------------------------*/
9 |
10 | %let config_root=/folders/myfolders/onedrive;
11 |
12 | %include "&config_root./onedrive_config.sas";
13 | %include "&config_root./onedrive_macros.sas";
14 |
15 | /*
16 | Our json file that contains the oauth token information
17 | */
18 | filename token "&config_root./token.json";
19 |
20 | /* Note: %if/%then in open code supported in 9.4m5 */
21 | %if (%sysfunc(fexist(token)) eq 0) %then %do;
22 | %put ERROR: &config_root./token.json not found. Run the setup steps to create the API tokens.;
23 | %end;
24 |
25 | /*
26 | If the access_token expires, we can just use the refresh token to get a new one.
27 |
28 | Some reasons the token (and refresh token) might not work:
29 | - Explicitly revoked by the app developer or admin
30 | - Password change in the user account for Microsoft Office 365
31 | - Time limit expiration
32 |
33 | Basically from this point on, user interaction is not needed.
34 |
35 | We assume that the token will only need to be refreshed once per session,
36 | and right at the beginning of the session.
37 |
38 | If a long running session is needed (>3600 seconds),
39 | then check API calls for a 401 return code
40 | and call %refresh if needed.
41 | */
42 |
43 | %process_token_file(token);
44 |
45 | /* If this is first use for the session, we'll likely need to refresh */
46 | /* the token. This will also call process_token_file again and update */
47 | /* our token.json file. */
48 | %refresh(&client_id.,&refresh_token.,&resource.,token,tenant=&tenant_id.);
49 |
50 | /*
51 | At this point we have a valid access token and we can start using the api.
52 | */
53 |
54 | /*
55 | First we need the ID of the "drive" we are going to use.
56 | to list the drives the current user has access to you can do this
57 | */
58 | filename resp TEMP;
59 | /* Note: oauth_bearer option added in 9.4M5 */
60 | proc http url="https://graph.microsoft.com/v1.0/me/drives/"
61 | oauth_bearer="&access_token"
62 | out = resp;
63 | run;
64 |
65 | libname jresp json fileref=resp;
66 |
67 | /*
68 | I only have access to 1 drive, but if you have multiple you can filter
69 | the set with a where clause on the name value.
70 |
71 | This creates a data set with the one record for the drive.
72 | */
73 | data drive;
74 | set jresp.value;
75 | run;
76 |
77 | /* store the ID value for the drive in a macro variable */
78 | proc sql noprint;
79 | select id into: driveId from drive;
80 | quit;
81 |
82 |
83 | /* LIST TOP LEVEL FOLDERS/FILES */
84 |
85 | /*
86 | To list the items in the drive, use the /children verb with the drive ID
87 | */
88 | filename resp TEMP;
89 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/root/children"
90 | oauth_bearer="&access_token"
91 | out = resp;
92 | run;
93 |
94 | libname jresp json fileref=resp;
95 |
96 | /* Create a data set with the top-level paths/files in the drive */
97 | data paths;
98 | set jresp.value;
99 | run;
100 |
101 | /* LIST ITEMS IN A SPECIFIC FOLDER */
102 |
103 | /*
104 | At this point, if you want to act on any of the items, you just replace "root"
105 | with the ID of the item. So to list the items in the "SASGF" folder I have:
106 | - find the ID for that folder
107 | - list the items within by using the "/children" verb
108 | */
109 |
110 | /* Find the ID of the folder I want */
111 | proc sql noprint;
112 | select id into: folderId from paths
113 | where name="SASGF";
114 | quit;
115 |
116 | filename resp TEMP;
117 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&folderId./children"
118 | oauth_bearer="&access_token"
119 | out = resp;
120 | run;
121 |
122 | /* This creates a data set of the items in that folder,
123 | which might include other folders.
124 | */
125 | libname jresp json fileref=resp;
126 | data folderItems;
127 | set jresp.value;
128 | run;
129 |
130 | /* DOWNLOAD A FILE FROM ONEDRIVE TO SAS SESSION */
131 |
132 | /*
133 | With a list of the items in this folder, we can download
134 | any item of interest by using the /content verb
135 | */
136 |
137 | /* Find the item with a certain name */
138 | proc sql noprint;
139 | select id into: fileId from folderItems
140 | where name="sas_tech_talks_18.xlsx";
141 | quit;
142 |
143 | filename fileout "&config_root./sas_tech_talks_18.xlsx";
144 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&fileId./content"
145 | oauth_bearer="&access_token"
146 | out = fileout;
147 | run;
148 |
149 | proc import file=fileout
150 | out=sasgf
151 | dbms=xlsx replace;
152 | run;
153 | /* UPLOAD A NEW FILE TO ONEDRIVE */
154 | /*
155 | We can upload a new file to that same folder with the PUT method and /content verb
156 | Notice the : after the folderId and the target filename
157 | */
158 |
159 | /* Create a simple Excel file to upload */
160 | %let targetFile=iris.xlsx;
161 | filename tosave "%sysfunc(getoption(WORK))/&targetFile.";
162 | ods excel(id=upload) file=tosave;
163 | proc print data=sashelp.iris;
164 | run;
165 | ods excel(id=upload) close;
166 |
167 | filename details temp;
168 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&folderId.:/&targetFile.:/content"
169 | method="PUT"
170 | in=tosave
171 | out=details
172 | oauth_bearer="&access_token";
173 | run;
174 |
175 | /*
176 | This returns a json response that describes the item uploaded.
177 | This step pulls out the main file attributes from that response.
178 | */
179 | libname attrs json fileref=details;
180 | data newfileDetails (keep=filename createdDate modifiedDate filesize);
181 | length filename $ 100 createdDate 8 modifiedDate 8 filesize 8;
182 | set attrs.root;
183 | filename = name;
184 | modifiedDate = input(lastModifiedDateTime,anydtdtm.);
185 | createdDate = input(createdDateTime,anydtdtm.);
186 | format createdDate datetime20. modifiedDate datetime20.;
187 | filesize = size;
188 | run;
189 |
190 | /* REPLACE AN EXISTING FILE IN ONEDRIVE */
191 |
192 | /*
193 | If you want to replace a file instead of making a new file
194 | then you need to upload it with the existing file ID. If you
195 | don't replace it with the existing ID, some sharing properties
196 | and history could be lost.
197 | */
198 |
199 | proc sql noprint;
200 | select id into: folderId from paths
201 | where name="SASGF";
202 | quit;
203 |
204 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&folderId./children"
205 | oauth_bearer="&access_token"
206 | out = resp;
207 | run;
208 |
209 | libname jresp json fileref=resp;
210 | data folderItems;
211 | set jresp.value;
212 | run;
213 |
214 | proc sql noprint;
215 | select id into: fileId from folderItems
216 | where name="iris.xlsx";
217 | quit;
218 |
219 | libname attrs json fileref=details;
220 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&fileId./content"
221 | method="PUT"
222 | in=tosave
223 | out=details
224 | oauth_bearer="&access_token";
225 | run;
226 |
227 | /*
228 | Capture the file details from the response
229 | */
230 | libname attrs json fileref=details;
231 | data replacefileDetails (keep=filename createdDate modifiedDate filesize id mimeType);
232 | length filename $ 100 createdDate 8 modifiedDate 8 filesize 8;
233 | merge attrs.root attrs.file;
234 | by ordinal_root;
235 | filename = name;
236 | modifiedDate = input(lastModifiedDateTime,anydtdtm.);
237 | createdDate = input(createdDateTime,anydtdtm.);
238 | format createdDate datetime20. modifiedDate datetime20.;
239 | filesize = size;
240 | run;
241 |
242 |
--------------------------------------------------------------------------------
/onedrive/onedrive_macros.sas:
--------------------------------------------------------------------------------
1 | /* ----------------------------------------------------
2 | Utility macros to manage and refresh the access token
3 | for using Microsoft OneDrive APIs from SAS,
4 | using PROC HTTP.
5 |
6 | Authors: Joseph Henry, SAS
7 | Chris Hemedinger, SAS
8 | Copyright 2018, SAS Institute Inc.
9 | -------------------------------------------------------*/
10 |
11 | /*
12 | Utility macro to process the JSON token
13 | file that was created at authorization time.
14 | This will fetch the access token, refresh token,
15 | and expiration datetime for the token so we know
16 | if we need to refresh it.
17 | */
18 | %macro process_token_file(file);
19 | libname oauth json fileref=&file.;
20 |
21 | data _null_;
22 | set oauth.root;
23 | call symputx('access_token', access_token,'G');
24 | call symputx('refresh_token', refresh_token,'G');
25 | /* convert epoch value to SAS datetime */
26 | call symputx('expires_on',(input(expires_on,best32.)+'01jan1970:00:00'dt),'G');
27 | run;
28 | %mend;
29 |
30 | /*
31 | Utility macro that retrieves the initial access token
32 | by redeeming the authorization code that you're granted
33 | during the interactive step using a web browser
34 | while signed into your Microsoft OneDrive / Azure account.
35 |
36 | This step also creates the initial token.json that will be
37 | used on subsequent steps/sessions to redeem a refresh token.
38 | */
39 | %macro get_token(client_id,
40 | code,
41 | resource,
42 | outfile,
43 | redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient,
44 | tenant=common,
45 | debug=0);
46 |
47 | proc http url="https://login.microsoft.com/&tenant_id./oauth2/token"
48 | method="POST"
49 | in="%nrstr(&client_id)=&client_id.%nrstr(&code)=&code.%nrstr(&redirect_uri)=&redirect_uri%nrstr(&grant_type)=authorization_code%nrstr(&resource)=&resource."
50 | out=&outfile.;
51 | %if &debug>=0 %then
52 | %do;
53 | debug level=&debug.;
54 | %end;
55 | %else %if &_DEBUG_. ge 1 %then
56 | %do;
57 | debug level=&_DEBUG_.;
58 | %end;
59 | run;
60 |
61 | %process_token_file(&outfile);
62 | %mend;
63 |
64 | /*
65 | Utility macro to redeem the refresh token
66 | and get a new access token for use in subsequent
67 | calls to the OneDrive service.
68 | */
69 | %macro refresh(client_id,
70 | refresh_token,
71 | resource,
72 | outfile,
73 | redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient,
74 | tenant=common,
75 | debug=0);
76 |
77 | proc http url="https://login.microsoft.com/&tenant_id./oauth2/token"
78 | method="POST"
79 | in="%nrstr(&client_id)=&client_id.%nrstr(&refresh_token=)&refresh_token%nrstr(&redirect_uri)=&redirect_uri.%nrstr(&grant_type)=refresh_token%nrstr(&resource)=&resource."
80 | out=&outfile.;
81 | %if &debug. ge 0 %then
82 | %do;
83 | debug level=&debug.;
84 | %end;
85 | %else %if %symexist(_DEBUG_) AND &_DEBUG_. ge 1 %then
86 | %do;
87 | debug level=&_DEBUG_.;
88 | %end;
89 | run;
90 |
91 | %process_token_file(&outfile);
92 | %mend refresh;
--------------------------------------------------------------------------------
/onedrive/onedrive_setup.sas:
--------------------------------------------------------------------------------
1 | /*
2 | This file contains steps that you perform just once to
3 | set up a new project. You'll use these steps to get an
4 | access code and your initial authentication tokens.
5 |
6 | You might need to repeat this step later if your
7 | Microsoft Office 365 credentials change (including password)
8 | or if the tokens are revoked by using another method.
9 |
10 | */
11 | %let config_root=/folders/myfolders/onedrive;
12 |
13 | %include "&config_root./onedrive_config.sas";
14 | %include "&config_root./onedrive_macros.sas";
15 |
16 | /*
17 | Our json file that contains the oauth token information
18 | */
19 | filename token "&config_root./token.json";
20 |
21 | /* Do these steps JUST ONCE, interactively,
22 | for your application and user account.
23 |
24 | Get the code from the URL in the browser and set it below
25 | as the value of auth_code.
26 |
27 | The authorization code is going to be a LONG character value.
28 | */
29 |
30 | /* Run this line to build the authorization URL */
31 | %let authorize_url=https://login.microsoftonline.com/&tenant_id./oauth2/authorize?client_id=&client_id.%nrstr(&response_type)=code%nrstr(&redirect_uri)=&redirect_uri.%nrstr(&resource)=&resource.;
32 | options nosource;
33 | %put Paste this URL into your web browser:;
34 | %put -- START -------;
35 | %put &authorize_url;
36 | %put ---END ---------;
37 | options source;
38 |
39 | /*
40 | Copy the value of the authorize_url into a web browser.
41 | Log into your OneDrive account as necessary.
42 | The browser will redirect to a new address.
43 | Copy the auth_code value from that address into the following
44 | macro variable. Then run the next two lines (including %get_token macro).
45 |
46 | Note: this code can be quite long -- 700+ characters.
47 | */
48 | %let auth_code=;
49 |
50 | /*
51 | Now that we have an authorization code we can get the access token
52 | This step will write the tokens.json file that we can use in our
53 | production programs.
54 | */
55 | %get_token(&client_id.,&auth_code,&resource.,token,tenant=&tenant_id,debug=3);
56 |
--------------------------------------------------------------------------------
/onedrive/onedrive_sharepoint_example.sas:
--------------------------------------------------------------------------------
1 | /* ----------------------------------------------------
2 | Example API calls from SAS to
3 | Microsoft Office 365 SharePoint Online.
4 |
5 | Authors: Joseph Henry, SAS
6 | Chris Hemedinger, SAS
7 | Copyright 2019, SAS Institute Inc.
8 | -------------------------------------------------------*/
9 |
10 | %let config_root=/folders/myfolders/onedrive;
11 |
12 | %include "&config_root./onedrive_config.sas";
13 | %include "&config_root./onedrive_macros.sas";
14 |
15 | /*
16 | Our json file that contains the oauth token information
17 | */
18 | filename token "&config_root./token.json";
19 |
20 | /* Note: %if/%then in open code supported in 9.4m5 */
21 | %if (%sysfunc(fexist(token)) eq 0) %then %do;
22 | %put ERROR: &config_root./token.json not found. Run the setup steps to create the API tokens.;
23 | %end;
24 |
25 | /*
26 | Now we have a file on disk named token.json.
27 | We should not need to interact with the web browser any more for quite a long time.
28 | If the access_token expires, we can just use the refresh token to get a new one.
29 |
30 | Some reasons the token (and refresh token) might not work:
31 | - Explicitly revoked by the app developer or admin
32 | - Password change in the user account for Microsoft Office 365
33 | - Time limit expiration
34 |
35 | Basically from this point on, user interaction is not needed.
36 |
37 | We can write code to see if the token need to be refreshed.
38 | We assume that the token will only need to be refreshed once per session,
39 | and right at the beginning of the session.
40 |
41 | If a long running session is needed (>3600 seconds),
42 | then check API calls for a 401 return code
43 | and call %refresh_token if needed.
44 | */
45 |
46 | %process_token_file(token);
47 |
48 | /* This "open code" %if-%then-%else is supported as of 9.4M5 */
49 | /* Check whether the token is aging out, and refresh if needed */
50 | %if &expires_on < %sysevalf(%sysfunc(datetime()) - %sysfunc(gmtoff())) %then %do;
51 | %refresh(&client_id.,&refresh_token.,&resource.,token,tenant=&tenant_id.);
52 | %end;
53 |
54 | filename resp temp;
55 |
56 | /* Note: oauth_bearer option added in 9.4M5 */
57 | /* Using the /sites methods in the Microsoft Graph API */
58 | /* May require the Sites.ReadWrite.All permission for your app */
59 | /* See https://docs.microsoft.com/en-us/graph/api/resources/sharepoint?view=graph-rest-1.0 */
60 | /* Set these values per your SharePoint Online site.
61 | Ex: https://yourcompany.sharepoint.com/sites/YourSite
62 | breaks down to:
63 | yourcompany.sharepoint.com -> hostname
64 | /sites/YourSite -> sitepath
65 |
66 | This example uses the /drive method to access the files on the
67 | Sharepoint site -- works just like OneDrive.
68 | API also supports a /lists method for SharePoint lists.
69 | Use the Graph Explorer app to find the correct APIs for your purpose.
70 | https://developer.microsoft.com/en-us/graph/graph-explorer
71 | */
72 | %let hostname = yourcompany.sharepoint.com;
73 | %let sitepath = /sites/YourSite;
74 | proc http url="https://graph.microsoft.com/v1.0/sites/&hostname.:&sitepath.:/drive"
75 | oauth_bearer="&access_token"
76 | out = resp;
77 | run;
78 |
79 | libname jresp json fileref=resp;
80 |
81 | /*
82 | This creates a data set with the one record for the drive.
83 | Need this object to get the Drive ID
84 | */
85 | data drive;
86 | set jresp.root;
87 | run;
88 |
89 | /* store the ID value for the drive in a macro variable */
90 | proc sql noprint;
91 | select id into: driveId from drive;
92 | quit;
93 |
94 | /* LIST TOP LEVEL FOLDERS/FILES */
95 |
96 | /*
97 | To list the items in the drive, use the /children verb with the drive ID
98 | */
99 | filename resp TEMP;
100 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/root/children"
101 | oauth_bearer="&access_token"
102 | out = resp;
103 | run;
104 |
105 | libname jresp json fileref=resp;
106 |
107 | /* Create a data set with the top-level paths/files in the drive */
108 | data paths;
109 | set jresp.value;
110 | run;
111 |
112 | /* LIST ITEMS IN A SPECIFIC FOLDER */
113 |
114 | /*
115 | At this point, if you want to act on any of the items, you just replace "root"
116 | with the ID of the item. So to list the items in the "General" folder I have:
117 | - find the ID for that folder
118 | - list the items within by using the "/children" verb
119 | */
120 |
121 | /* Find the ID of the folder I want */
122 | proc sql noprint;
123 | select id into: folderId from paths
124 | where name="General";
125 | quit;
126 |
127 | filename resp TEMP;
128 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&folderId./children"
129 | oauth_bearer="&access_token"
130 | out = resp;
131 | run;
132 |
133 |
134 | /* This creates a data set of the items in that folder,
135 | which might include other folders.
136 | */
137 | libname jresp json fileref=resp;
138 | data folderItems;
139 | set jresp.value;
140 | run;
141 |
142 |
143 | /* DOWNLOAD A FILE FROM SharePoint TO SAS SESSION */
144 |
145 | /*
146 | With a list of the items in this folder, we can download
147 | any item of interest by using the /content verb
148 | */
149 |
150 | /* Find the item with a certain name */
151 | /* My example uses an Excel file called "pmessages.xlsx */
152 | /* Which this downloads and then imports as SAS data */
153 | proc sql noprint;
154 | select id into: fileId from folderItems
155 | where name="pmessages.xlsx";
156 | quit;
157 |
158 | filename fileout "&config_root./pmessages.xlsx";
159 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&fileId./content"
160 | oauth_bearer="&access_token"
161 | out = fileout;
162 | run;
163 |
164 | proc import file=fileout
165 | out=pmessages
166 | dbms=xlsx replace;
167 | run;
168 |
169 |
170 | /* UPLOAD A NEW FILE TO SharePoint */
171 | /*
172 | We can upload a new file to that same folder with the PUT method and /content verb
173 | Notice the : after the folderId and the target filename
174 | */
175 |
176 | /* Create a simple Excel file to upload */
177 | %let targetFile=iris.xlsx;
178 | filename tosave "%sysfunc(getoption(WORK))/&targetFile.";
179 | ods excel(id=upload) file=tosave;
180 | proc print data=sashelp.iris;
181 | run;
182 | ods excel(id=upload) close;
183 |
184 | filename details temp;
185 | proc http url="https://graph.microsoft.com/v1.0/me/drives/&driveId./items/&folderId.:/&targetFile.:/content"
186 | method="PUT"
187 | in=tosave
188 | out=details
189 | oauth_bearer="&access_token";
190 | run;
191 |
192 | /*
193 | This returns a json response that describes the item uploaded.
194 | This step pulls out the main file attributes from that response.
195 | */
196 | libname attrs json fileref=details;
197 | data newfileDetails (keep=filename createdDate modifiedDate filesize);
198 | length filename $ 100 createdDate 8 modifiedDate 8 filesize 8;
199 | set attrs.root;
200 | filename = name;
201 | modifiedDate = input(lastModifiedDateTime,anydtdtm.);
202 | createdDate = input(createdDateTime,anydtdtm.);
203 | format createdDate datetime20. modifiedDate datetime20.;
204 | filesize = size;
205 | run;
206 |
--------------------------------------------------------------------------------
/precision/README.md:
--------------------------------------------------------------------------------
1 | ## Precision in SAS numbers and IEEE floating point math
2 |
3 | This sample program and output are companions [to this blog post that describes
4 | how floating point numbers](https://blogs.sas.com/content/sasdummy/precision-in-sas-numbers/) are represented in SAS (and in many other programming languages). Percieved precision errors are often just a byproduct of how modern computers represent decimal numbers.
5 |
6 | * [precision_example.sas](./precision_example.sas)
7 | * [Precision example output](./precision-output.html)
8 |
9 | ### Summary
10 |
11 | * SAS fully exploits the hardware on which it runs to calculate correct and complete results using numbers of high precision and large magnitude.
12 |
13 | * By rounding to 15 significant digits, the _w.d_ format maps a range of base-2 values to each base-10 value. This is usually what you want, but not when you're digging into very small differences.
14 |
15 | * Numeric operations in the DATA step use all of the range and precision supported by the hardware. This can be more than you had in mind, and includes more precision than the _w.d_ format displays.
16 |
17 | * SAS has a rich set of tools that the savvy SAS consultant can employ to diagnose unexpected behavior with floating-point numbers.
--------------------------------------------------------------------------------
/precision/precision_example.sas:
--------------------------------------------------------------------------------
1 | /*-------------------------------------------------------------------
2 | -------------------------------------------------------------------*/
3 | options stimer fullstimer linesize=120;
4 | options mprint symbolgen spool;
5 |
6 | /*-------------------------------------------------------------------
7 | This macro tests each bit in a byte of the mantissa of a
8 | floating-point number. At each bit position, if the bit is
9 | set, the macro accumulates the value that bit contributes
10 | to the mantissa.
11 |
12 | Set the value of "mask" so that the first division operation
13 | will produce the first mask value to actually use.
14 | -------------------------------------------------------------------*/
15 | %macro getbyte( varname, charpos, bitpos );
16 | %let mask = 256; /* 0100x */
17 |
18 | /*-------------------------------------------------------------------
19 | Read the byte of the mantissa into a numeric variable so the
20 | masking can be done in the less-confusing "human-readable"
21 | order instead of the literal Intel little-endian order.
22 | -------------------------------------------------------------------*/
23 | thisbyte_n = input( substr(&varname, &charpos, 1), pib1. );
24 |
25 | /*-------------------------------------------------------------------
26 | Step through the bits from most-significant to least-significant.
27 |
28 | Note that the mask value will progress from 128 (0x80), to
29 | 64 (0x40), to 32 (0x20), and so on.
30 |
31 | If the bit is set, accumulate the value of that bit position.
32 | -------------------------------------------------------------------*/
33 | %do i = 0 %to 7 %by 1;
34 | %let thisbit = %eval( &bitpos + &i );
35 | %let mask = %eval( &mask / 2 );
36 | if ( band(thisbyte_n, &mask) ) then value + 2**(exp_use - &thisbit);
37 | %end;
38 | %mend;
39 |
40 | /*-------------------------------------------------------------------
41 | This macro takes a number and generates the five-number sequence
42 | from two below and two above the supplied number.
43 | -------------------------------------------------------------------*/
44 | %macro surround( value, extension, increment );
45 | series = &value;
46 |
47 | do x = (&value - &extension) to (&value + &extension) by &increment;
48 | %decode_fp_ieee( x );
49 | output;
50 | end;
51 | %mend;
52 |
53 | /*-------------------------------------------------------------------
54 | At each number in the sequence, this macro decodes the components
55 | of the IEEE floating-point representation of the value.
56 | -------------------------------------------------------------------*/
57 | %macro decode_fp_ieee( varname );
58 |
59 | /*-------------------------------------------------------------------
60 | The RB8. format just copies the bytes *exactly* to a character
61 | variable. This gives the program access to the raw floating-
62 | point number.
63 | -------------------------------------------------------------------*/
64 | crb&varname = put(&varname, rb8.);
65 |
66 | /*-------------------------------------------------------------------
67 | The sign and exponent are 12 of the 16 rightmost bits.
68 |
69 | Record the value of the sign bit as a character string that's
70 | ready to print.
71 | -------------------------------------------------------------------*/
72 | signexp = input( substr(crb&varname, 7, 2), pib2. );
73 |
74 | if ( band(signexp, 08000x) ) then sign = '1 (-)';
75 | else sign = '0 (+)';
76 |
77 | /*-------------------------------------------------------------------
78 | Pull the 11-bit exponent out. Use the band() function to mask
79 | out the 11 exponent bits, then use brshift() to shift them
80 | right so they form the 12 least-significant bits of an integer.
81 | -------------------------------------------------------------------*/
82 | exponent = band(signexp, 07ff0x);
83 | exponent = brshift(exponent, 4);
84 |
85 | /*-------------------------------------------------------------------
86 | An IEEE floating-point exponent is "biased" so that its range
87 | of zero to 2047 can be used as -1023 to 1024.
88 | (see en.wikipedia.org/wiki/IEEE_754-1985)
89 | -------------------------------------------------------------------*/
90 | exp_biased = exponent;
91 | exp_use = exponent - 1023;
92 |
93 | /*-------------------------------------------------------------------
94 | Initialize the calculated value to 2 raised to the de-biased
95 | value of the exponent.
96 | -------------------------------------------------------------------*/
97 | value = 2**exp_use;
98 |
99 | /*-------------------------------------------------------------------
100 | Walk down the four most-significant bits of the exponent. If
101 | a bit is set, accumulate the value of that bit position.
102 | -------------------------------------------------------------------*/
103 | thisbyte_n = input( substr(crb&varname, 7, 1), pib1. );
104 |
105 | if ( band(thisbyte_n, 08x) ) then value + 2**(exp_use - 1);
106 | if ( band(thisbyte_n, 04x) ) then value + 2**(exp_use - 2);
107 | if ( band(thisbyte_n, 02x) ) then value + 2**(exp_use - 3);
108 | if ( band(thisbyte_n, 01x) ) then value + 2**(exp_use - 4);
109 |
110 | /*-------------------------------------------------------------------
111 | Use the macro to walk down the rest of the mantissa.
112 | -------------------------------------------------------------------*/
113 | %getbyte( crb&varname, 6, 5 );
114 | %getbyte( crb&varname, 5, 13 );
115 | %getbyte( crb&varname, 4, 21 );
116 | %getbyte( crb&varname, 3, 29 );
117 | %getbyte( crb&varname, 2, 37 );
118 | %getbyte( crb&varname, 1, 45 );
119 | %mend;
120 |
121 | /*-------------------------------------------------------------------
122 | Un-comment these two steps to explore fractional values.
123 | -------------------------------------------------------------------*/
124 |
125 | /*-------------------------------------------------------------------
126 | data;
127 | drop thisbyte_n;
128 | length crbx $ 8
129 | sign $ 5;
130 | format crbx $hex16.
131 | x value 15.12
132 | exp_biased exp_use 5.
133 | signexp exponent hex4.;
134 |
135 | do x = 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125;
136 | %decode_fp_ieee( x );
137 | output;
138 | end;
139 | run;
140 |
141 | title "1: Software Calculation of Fractional IEEE Floating-Point Values, Intel Format";
142 | proc print;
143 | var crbx x value signexp sign exponent exp_biased exp_use;
144 | run;
145 | -------------------------------------------------------------------*/
146 |
147 | /*-------------------------------------------------------------------
148 | This DATA step uses the above macros to decode sample
149 | floating-point numbers.
150 | -------------------------------------------------------------------*/
151 | data;
152 | drop thisbyte_n;
153 | length crbx $ 8
154 | sign $ 5;
155 | format crbx $hex16.
156 | x value comma10.
157 | exp_biased exp_use 5.
158 | signexp exponent hex4.;
159 |
160 | do x = 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125;
161 | %decode_fp_ieee( x );
162 | end;
163 |
164 | %surround( 0, 5, 1 );
165 | %surround( 8, 2, 1 );
166 | %surround( 16, 2, 1 );
167 | %surround( 32, 2, 1 );
168 | %surround( 64, 2, 1 );
169 | %surround( 128, 2, 1 );
170 | %surround( 256, 2, 1 );
171 | %surround( 512, 1, 1 );
172 | %surround( 1024, 1, 1 );
173 | %surround( 2048, 1, 1 );
174 | %surround( 4096, 1, 1 );
175 | %surround( 8192, 1, 1 );
176 | %surround( 16384, 1, 1 );
177 | %surround( 32768, 1, 1 );
178 | %surround( 65536, 1, 1 );
179 | %surround( 131072, 1, 1 );
180 | %surround( 262144, 1, 1 );
181 | %surround( 524288, 1, 1 );
182 | %surround( 1048576, 1, 1 );
183 | %surround( 2097152, 1, 1 );
184 | %surround( 4194304, 1, 1 );
185 | %surround( 8388608, 1, 1 );
186 | run;
187 |
188 | title "1: Software Calculation of IEEE Floating-Point Values, Intel Format";
189 | proc print;
190 | by notsorted series;
191 | id series;
192 | var crbx x value signexp sign exponent exp_biased exp_use;
193 | run;
194 |
195 | /*-------------------------------------------------------------------
196 | This DATA step creates the "root" data file that will be used
197 | by most of the steps below. It captures the two original
198 | values and sets up formats so the variables can be read in
199 | both hexadecimal (crb...) and decimal.
200 | -------------------------------------------------------------------*/
201 | data a;
202 | length crbx crby crbd $ 8
203 | sign $ 5;
204 | format x y 22.7
205 | d 11.8
206 | exp_biased exp_use 5.
207 | signexp exponent hex4.
208 | crbx crby crbd $hex16.;
209 | drop thisbyte_n;
210 |
211 | /*-------------------------------------------------------------------
212 | Set x to the original integer value, and y to the original
213 | value that mysteriously compared unequal to x. Calculate
214 | the difference between y and x.
215 | -------------------------------------------------------------------*/
216 | x = 122000015596951.0;
217 | y = 122000015596951.015625;
218 | d = y - x;
219 |
220 | /*-------------------------------------------------------------------
221 | Save the IEEE floating-point representations as character
222 | strings that can be displayed in hexadecimal to show the
223 | actual binary floating-point representations.
224 | -------------------------------------------------------------------*/
225 | crbx = put(x, rb8.);
226 | crby = put(y, rb8.);
227 | crbd = put(d, rb8.);
228 |
229 | /*-------------------------------------------------------------------
230 | Take the difference value apart to show how normalization
231 | moves the mantissa and re-calculates the exponent.
232 | -------------------------------------------------------------------*/
233 | %decode_fp_ieee( d );
234 | run;
235 |
236 | title "2: The Two Original Values and Their Difference";
237 | proc print;
238 | var x crbx y crby d crbd signexp exponent exp_biased exp_use;
239 | run;
240 |
241 | /*-------------------------------------------------------------------
242 | Examine values around the original value.
243 | -------------------------------------------------------------------*/
244 | data;
245 | length c $ 1;
246 | format c $hex2.
247 | lsb binary8.;
248 |
249 | set a;
250 |
251 | /*-------------------------------------------------------------------
252 | Go through a loop that sets the least-significant byte of the
253 | floating-point value to a series of values that increment by
254 | one bit.
255 | -------------------------------------------------------------------*/
256 | do lsb = (0c0x - 15) to (0c0x + 15) by 1;
257 |
258 | /*-------------------------------------------------------------------
259 | Store the loop variable as a one-byte integer, then store that
260 | in the least-significant byte of the floating-point value.
261 | -------------------------------------------------------------------*/
262 | c = put( lsb, pib1. );
263 | substr( crby, 1, 1 ) = c;
264 |
265 | /*-------------------------------------------------------------------
266 | Copy the floating-point bit string into a numeric variable.
267 |
268 | Calculate the difference between this floating-point value and
269 | Kugendrn's customer's original value.
270 |
271 | Calculate the difference between this observation's difference
272 | and the previous observation's difference.
273 | -------------------------------------------------------------------*/
274 | y = input(crby, rb8.);
275 | d = y - x;
276 | lagd = d - lag(d);
277 | output;
278 | end;
279 | run;
280 |
281 | title "3: Values Immediately Adjacent to the Customer's Integer";
282 | proc print;
283 | var x crbx y crby lsb d lagd;
284 | run;
285 |
286 | /*-------------------------------------------------------------------
287 | These steps are commented-out because discussing them made the
288 | mail message too long. But feel free to uncomment them and
289 | explore this area yourself.
290 | -------------------------------------------------------------------*/
291 |
292 | /*-------------------------------------------------------------------
293 | Create an interleaved series of values from the original
294 | integer value to a little more than one greater than itself.
295 |
296 | The interleaving will be of values produced by base-2
297 | arithmetic and base-10 arithmetic.
298 |
299 | Note that this loop has to set the two least-significant bytes
300 | of the floating-point value.
301 | -------------------------------------------------------------------*/
302 | * data c1;
303 | * drop c;
304 | * length c $ 2;
305 | * format c $hex4.
306 | * i_2 hex4.;
307 | * retain base ' 2';
308 |
309 | /*-------------------------------------------------------------------
310 | Read the original values, then loop from the original integer
311 | to a little past one greater than it. This is to get past
312 | the "boundary effects" at the change of displayed values.
313 | -------------------------------------------------------------------*/
314 | * set a;
315 | *
316 | * do i_2 = 65c0x to 6602x by 1;
317 |
318 | /*-------------------------------------------------------------------
319 | Save the loop value as an integer in the least-significant
320 | two bytes of the floating-point value. Then load the
321 | modified floating-point value into a numeric variable.
322 | -------------------------------------------------------------------*/
323 | * c = put( i_2, pib2. );
324 | * substr( crby, 1, 2 ) = c;
325 | * y = input(crby, rb8.);
326 |
327 | /*-------------------------------------------------------------------
328 | Compare the just-produced floating-point value to the original
329 | integer value, and write the observation.
330 | -------------------------------------------------------------------*/
331 | * d = y - x;
332 | * output;
333 | * end;
334 | * run;
335 |
336 | /*-------------------------------------------------------------------
337 | Now produce a similar series of observations, but incremented
338 | by a base-10 value.
339 | -------------------------------------------------------------------*/
340 | * data c2;
341 | * drop cx;
342 | * length cx $ 8;
343 | * retain base '10';
344 | * format i_10 3.1;
345 |
346 | /*-------------------------------------------------------------------
347 | Get the original values, then loop to the next-higher-by-one
348 | value, using an increment that's one power of 10 lower than
349 | the range we're covering (so we'll get 10 steps).
350 | -------------------------------------------------------------------*/
351 | * set a;
352 | *
353 | * do y = x to (x + 1) by 0.1;
354 |
355 | /*-------------------------------------------------------------------
356 | Save the floating-point value so it can be displayed.
357 |
358 | Calculate the difference and save it for display.
359 |
360 | The "i_10"variable is for display; it shows the index by which
361 | "y" was incremented on this iteration of the loop.
362 | -------------------------------------------------------------------*/
363 | * crby = put(y, rb8.);
364 | * d = y - x;
365 | * output;
366 | * i_10 + 0.1;
367 | * end;
368 |
369 | /*-------------------------------------------------------------------
370 | Just show this in the log for reference.
371 | -------------------------------------------------------------------*/
372 | * cx = put( 0.1, rb8. );
373 | * put cx= $hex16.;
374 | * run;
375 |
376 | /*-------------------------------------------------------------------
377 | Combine the two SAS data files generated above.
378 | -------------------------------------------------------------------*/
379 | * data;
380 | * drop i_2;
381 | * length c_2 $ 4;
382 | *
383 | * set c1(in=in_1)
384 | * c2(in=in_2);
385 |
386 | /*-------------------------------------------------------------------
387 | For readability in the output, blank whichever type of
388 | increment did *not* contribute.
389 | -------------------------------------------------------------------*/
390 | * if ( in_1 ) then c_2 = put(i_2, hex4.);
391 | * else c_2 = ' ';
392 | *
393 | * if ( not in_2 ) then i_10 = .;
394 | * run;
395 |
396 | /*-------------------------------------------------------------------
397 | Sort the combined file so the values incremented using
398 | base-2 and base-10 interleave to show how the machine
399 | did the math.
400 | -------------------------------------------------------------------*/
401 | * proc sort;
402 | * by d;
403 | * run;
404 | *
405 | * title "0: Interleaving Values From Base-2 and Base-10 Addition";
406 | * proc print;
407 | * var base x crbx y crby c_2 i_10 d;
408 | * run;
409 |
410 | /*-------------------------------------------------------------------
411 | Create a file that covers a wider range and shows the number
412 | of base-2 values that are between each base-10 value.
413 | -------------------------------------------------------------------*/
414 | data;
415 | drop x crbx d crbd thisbyte_n;
416 | format ls_12bits_hex hex3.
417 | ls_12bits_bin binary12.
418 | value 8.3
419 | d_10 3.;
420 | label value = 'Lowest 12 Bits Decoded';
421 |
422 | /*-------------------------------------------------------------------
423 | Get the original values and initiate a loop from five below
424 | to five above the original value. On each iteration,
425 | capture the floating-point bit pattern into a character
426 | variable where it can be decoded and displayed.
427 | -------------------------------------------------------------------*/
428 | set a;
429 |
430 | do y = (x - 5) to (x + 5) by 1;
431 | crby = put(y, rb8.);
432 |
433 | /*-------------------------------------------------------------------
434 | All of the action is in the least-significant 12 bits of the
435 | mantissa. To get 12 bits, we have to read 2 bytes (16 bits).
436 |
437 | Because Intel is byte-swapped, we have to read the individual
438 | bytes in significance, not storage, order.
439 | -------------------------------------------------------------------*/
440 | ls_12bits_hex = input(substr(crby, 2, 1), pib1.);
441 | ls_12bits_hex = (ls_12bits_hex * 256) + input(substr(crby, 1, 1), pib1.);
442 | ls_12bits_bin = ls_12bits_hex;
443 |
444 | /*-------------------------------------------------------------------
445 | Calculate the difference between this difference and the
446 | previous one. Copy it to a variable that will be displayed
447 | using another format.
448 | -------------------------------------------------------------------*/
449 | d_10 = ls_12bits_hex - lag(ls_12bits_hex);
450 |
451 | /*-------------------------------------------------------------------
452 | Extract the sign and exponent. De-bias the exponent.
453 | -------------------------------------------------------------------*/
454 | signexp = input( substr(crby, 7, 2), pib2. );
455 |
456 | exponent = band(signexp, 07ff0x);
457 | exponent = brshift(exponent, 4);
458 | exp_biased = exponent;
459 | exp_use = exponent - 1023;
460 |
461 | /*-------------------------------------------------------------------
462 | Decode only the least-significant 12 bits of the mantissa.
463 | This covers all of the variation in this range of values.
464 | -------------------------------------------------------------------*/
465 | value = 0;
466 |
467 | thisbyte_n = input( substr(crby, 2, 1), pib1. );
468 |
469 | if ( band(thisbyte_n, 08x) ) then value + 2**(exp_use - 41);
470 | if ( band(thisbyte_n, 04x) ) then value + 2**(exp_use - 42);
471 | if ( band(thisbyte_n, 02x) ) then value + 2**(exp_use - 43);
472 | if ( band(thisbyte_n, 01x) ) then value + 2**(exp_use - 44);
473 |
474 | %getbyte( crby, 1, 45 );
475 |
476 | output;
477 | end;
478 | run;
479 |
480 | title "4: Incrementing the Least-Significant of 15 Significant Decimal Digits";
481 | proc print label;
482 | var y crby ls_12bits_hex ls_12bits_bin d_10 exp_use value;
483 | run;
484 |
485 | /*-------------------------------------------------------------------
486 | We showed what happens when these values participate in a
487 | subtraction operation above. Here, show what happens when
488 | the operations are divide, multiply, and add. And show the
489 | results of the fuzz() and round() functions.
490 | -------------------------------------------------------------------*/
491 | data;
492 | length crbw operation $ 8;
493 | format w 22.7
494 | d E12.7
495 | crbw $hex16.;
496 | set a;
497 |
498 | operation = 'Divide';
499 | w = y / x;
500 | crbw = put(w, rb8.);
501 | d = w - 1;
502 | output;
503 |
504 | operation = 'Multiply';
505 | w = y * x;
506 | crbw = put(w, rb8.);
507 | d = w - (x * x);
508 | output;
509 |
510 | operation = 'Add';
511 | w = y + x;
512 | crbw = put(w, rb8.);
513 | d = w - (x + x);
514 | output;
515 |
516 | operation = 'Fuzz';
517 | w = fuzz(y);
518 | crbw = put(w, rb8.);
519 | d = w - fuzz(x);
520 | output;
521 |
522 | operation = 'Round';
523 | w = round(y);
524 | crbw = put(w, rb8.);
525 | d = w - round(x);
526 | output;
527 | run;
528 |
529 | title "5: Results of Miscellaneous Math Operations";
530 | proc print;
531 | id operation;
532 | var x crbx y crby w crbw d;
533 | run;
534 |
--------------------------------------------------------------------------------
/prochttp/basicauth.sas:
--------------------------------------------------------------------------------
1 | filename resp temp;
2 | proc http
3 | url="http://httpbin.org/basic-auth/chris/pass125"
4 | method="GET"
5 | AUTH_BASIC
6 | out=resp
7 | webusername="chris"
8 | webpassword="pass125"
9 | ;
10 | run;
11 |
12 | data _null_;
13 | rc = jsonpp('resp','log');
14 | run;
--------------------------------------------------------------------------------
/prochttp/cms_nursinghome.sas:
--------------------------------------------------------------------------------
1 | filename nh temp;
2 | proc http
3 | url="https://data.cms.gov/api/views/s2uc-8wxp/rows.csv?accessType=DOWNLOAD"
4 | method="GET"
5 | out=nh;
6 | run;
7 |
8 |
9 |
10 | options validvarname=v7;
11 | proc import file=nh
12 | out=covid19nh
13 | dbms=csv
14 | replace;
15 | run;
16 |
17 |
--------------------------------------------------------------------------------
/prochttp/currency.sas:
--------------------------------------------------------------------------------
1 | filename data URL "https://www.federalreserve.gov/paymentsystems/files/coin_currcircvolume.txt";
2 |
3 | filename data "c:\temp\curr.txt";
4 | proc http method="GET"
5 | url =
6 | "https://www.federalreserve.gov/paymentsystems/files/coin_currcircvolume.txt"
7 | out=data;
8 | run;
9 |
10 | data work.fromfed;
11 | length
12 | year 8
13 | notes_1 8
14 | notes_2 8
15 | notes_5 8
16 | notes_10 8
17 | notes_20 8
18 | notes_50 8
19 | notes_100 8
20 | notes_500plus 8 ;
21 | infile data
22 | firstobs=4
23 | encoding="utf-8"
24 | truncover ;
25 | input
26 | @1 year
27 | @13 notes_1
28 | @22 notes_2
29 | @30 notes_5
30 | @38 notes_10
31 | @46 notes_20
32 | @54 notes_50
33 | @61 notes_100
34 | @69 notes_500plus ;
35 |
36 | /* drop empty rows */
37 | if year ^= .;
38 | run;
39 |
40 |
41 | /* Add and ID value to prepare for transpose */
42 | proc sql ;
43 | create table circdata as select t1.*,
44 | 'BillCountsInBillions' as ID from fromfed t1
45 | order by year;
46 | run;
47 |
48 | /* Stack this data into column-wise format */
49 | proc transpose data = work.circdata
50 | out=work.stackedcash
51 | name=denomination
52 | label=countsinbillions
53 | ;
54 | by year;
55 | id id;
56 | var notes_1 notes_2 notes_5 notes_10 notes_20 notes_50 notes_100 notes_500plus;
57 | run;
58 |
59 | /* Calculate the dollar values based on counts */
60 | data cashvalues;
61 | set stackedcash;
62 | length multiplier 8 value 8;
63 | select (denomination);
64 | when ('notes_1') multiplier=1;
65 | when ('notes_2') multiplier=2;
66 | when ('notes_5') multiplier=5;
67 | when ('notes_10') multiplier=10;
68 | when ('notes_20') multiplier=20;
69 | when ('notes_50') multiplier=50;
70 | when ('notes_100') multiplier=100;
71 | when ('notes_500plus') multiplier=500;
72 | otherwise multiplier=0;
73 | end;
74 | value = BillCountsInBillions * multiplier;
75 | run;
76 |
77 | /* Use a format to make a friendlier legend in our plots */
78 | proc format lib=work;
79 | value $notes
80 | "notes_1" = "$1"
81 | "notes_2" = "$2"
82 | "notes_5" = "$5"
83 | "notes_10" = "$10"
84 | "notes_20" = "$20"
85 | "notes_50" = "$50"
86 | "notes_100" = "$100"
87 | "notes_500plus" = "$500+"
88 | ;
89 | run;
90 |
91 | proc freq data=cashvalues
92 | order=data
93 | noprint
94 | ;
95 | tables denomination / nocum out=work.cashpercents scores=table;
96 | weight value;
97 | by year;
98 | run;
99 |
100 | proc freq data=cashvalues
101 | order=data
102 | noprint
103 | ;
104 | tables denomination / nocum out=work.billpercents scores=table;
105 | weight BillCountsInBillions;
106 | by year;
107 | run;
108 |
109 | /* directives for EG HTML output */
110 | ods html5 (id=eghtml) gtitle gfootnote;
111 | ods graphics / width=700px height=400px;
112 |
113 | /* Plot the results */
114 | footnote height=1 'Source: https://www.federalreserve.gov/paymentsystems/coin_currcircvolume.htm';
115 | title "US Currency in Circulation: % Value of Denominations";
116 | proc sgplot data=cashpercents ;
117 | label denomination = 'Denomination';
118 | format denomination $notes.;
119 | vbar year / response=percent group=denomination grouporder=data;
120 | yaxis label="% Value in Billions" ;
121 | xaxis display=(nolabel);
122 | keylegend / position=right across=1 noborder valueattrs=(size=12pt) title="" ;
123 | run;
124 |
125 | title "US Currency in Circulation: % Bill Counts";
126 | proc sgplot data=billpercents ;
127 | label denomination = 'Denomination';
128 | format denomination $notes.;
129 | vbar year / response=percent group=denomination grouporder=data;
130 | yaxis label="% Bills in Billions" ;
131 | xaxis display=(nolabel);
132 | keylegend / position=right across=1 noborder valueattrs=(size=12pt) title="";
133 | run;
--------------------------------------------------------------------------------
/prochttp/httppost.sas:
--------------------------------------------------------------------------------
1 | filename resp temp;
2 | proc http
3 | url="http://httpbin.org/post"
4 | method="POST"
5 | in="custname=Joe%str(&)size=large%str(&)topping=cheese"
6 | out=resp;
7 | run;
8 |
9 | data _null_;
10 | rc = jsonpp('resp','log');
11 | run;
12 |
13 |
--------------------------------------------------------------------------------
/prochttp/prochttp_test.sas:
--------------------------------------------------------------------------------
1 | /* PROC HTTP and JSON libname test */
2 | /* Requires SAS 9.4m4 or later to run */
3 | /* SAS University Edition Dec 2017 or later */
4 |
5 | filename resp "%sysfunc(getoption(WORK))/stream.json";
6 | proc http
7 | url="https://httpbin.org/stream/1"
8 | method="GET"
9 | out=resp;
10 | run;
11 |
12 | /* Supported with SAS 9.4 Maint 5 */
13 | %put HTTP Status code = &SYS_PROCHTTP_STATUS_CODE. : &SYS_PROCHTTP_STATUS_PHRASE.;
14 |
15 |
16 | data _null_;
17 | rc = jsonpp('resp','log');
18 | run;
19 |
20 | /* Tell SAS to parse the JSON response */
21 | libname stream JSON fileref=resp;
22 |
23 | title "JSON library structure";
24 | proc datasets lib=stream;
25 | quit;
26 |
27 | data all;
28 | set stream.alldata;
29 | run;
30 |
31 | libname stream clear;
32 | filename resp clear;
--------------------------------------------------------------------------------
/prochttp/webscrape.sas:
--------------------------------------------------------------------------------
1 | /* Get all of the nonblank lines */
2 | filename CDC url "https://wwwn.cdc.gov/nndss/conditions/search/";
3 | data rep;
4 | infile CDC length=len lrecl=32767;
5 | input line $varying32767. len;
6 | line = strip(line);
7 | if len>0;
8 | run;
9 | filename CDC clear;
10 |
11 | /* Parse the lines and keep just condition names */
12 | /* When a condition code is found, grab the line following (full name of condition) */
13 | /* and the 8th line following (Notification To date) */
14 | /* Relies on this page's exact layout and line break scheme */
15 | data parsed (keep=condition_code condition_full note_to);
16 | length condition_code $ 40 condition_full $ 60;
17 | set rep;
18 | if find(line,"/nndss/conditions/") then do;
19 | condition_code=scan(line,4,'/');
20 | pickup= _n_+1 ;
21 | pickup2 = _n_+8;
22 | set rep (rename=(line=condition_full)) point=pickup;
23 | set rep (rename=(line=note_to)) point=pickup2;
24 | output;
25 | end;
26 | run;
--------------------------------------------------------------------------------
/prochttp/whoseinspace.sas:
--------------------------------------------------------------------------------
1 | options ps=max;
2 | /* Neat service from Open Notify project */
3 | filename resp temp;
4 | proc http
5 | url="http://api.open-notify.org/astros.json"
6 | method= "GET"
7 | out=resp;
8 | run;
9 |
10 | data _null_;
11 | rc = jsonpp('resp','log');
12 | run;
13 |
14 | /* Assign a JSON library to the HTTP response */
15 | libname space JSON fileref=resp;
16 |
--------------------------------------------------------------------------------
/rng_example_thanos.sas:
--------------------------------------------------------------------------------
1 | /* ----------------------------------------------------
2 | Which RNG did Thanos use?
3 | https://blogs.sas.com/content/sasdummy/rng-avengers-thanos/
4 | Authors: Chris Hemedinger, SAS
5 | Copyright 2018, SAS Institute Inc.
6 | -------------------------------------------------------*/
7 |
8 | /* Using STREAMINIT with the new RNG algorithm argument */
9 | %let algorithm = PCG;
10 | data characters;
11 | call streaminit("&algorithm.",2018);
12 | infile datalines dsd;
13 | retain x 0 y 1;
14 | length Name $ 60 spared 8 x 8 y 8;
15 | input Name;
16 | Spared = rand("Bernoulli", 0.5);
17 | x+1;
18 | if x > 10 then
19 | do; y+1; x = 1;end;
20 | /* Character data from IMDB.com for Avengers: Infinity War */
21 | datalines;
22 | Tony Stark / Iron Man
23 | Thor
24 | Bruce Banner / Hulk
25 | Steve Rogers / Captain America
26 | Natasha Romanoff / Black Widow
27 | James Rhodes / War Machine
28 | Doctor Strange
29 | Peter Parker / Spider-Man
30 | T'Challa / Black Panther
31 | Gamora
32 | Nebula
33 | Loki
34 | Vision
35 | Wanda Maximoff / Scarlet Witch
36 | Sam Wilson / Falcon
37 | Bucky Barnes / Winter Soldier
38 | Heimdall
39 | Okoye
40 | Eitri
41 | Wong
42 | Mantis
43 | Drax
44 | Groot (voice)
45 | Rocket (voice)
46 | Pepper Potts
47 | The Collector
48 | Thanos
49 | Peter Quill / Star-Lord
50 | On-Set Rocket
51 | Secretary of State Thaddeus Ross
52 | Shuri
53 | Cull Obsidian / On-Set Groot
54 | Ebony Maw
55 | Proxima Midnight
56 | Corvus Glaive (as Michael Shaw)
57 | Bus Driver
58 | M'Baku
59 | Ayo
60 | Voice of Friday (voice)
61 | On-Set Proxima Midnight
62 | Ned
63 | Cindy
64 | Sally
65 | Tiny
66 | Young Gamora
67 | Gamora's Mother
68 | Red Skull (Stonekeeper)
69 | Secretary Ross' Aide
70 | Secretary Ross' Aide
71 | Doctor Strange Double
72 | Thanos Reader
73 | Teenage Groot Reader
74 | Street Pedestrian #1
75 | Street Pedestrian #2
76 | Scottish News (STV) Reporter
77 | Dora Milaje (uncredited)
78 | NYPD (uncredited)
79 | Mourner (uncredited)
80 | Merchant (uncredited)
81 | Student (uncredited)
82 | NYC Pedestrian (uncredited)
83 | Taxi Cab Driver (uncredited)
84 | Soldier (uncredited)
85 | NYC Pedestrian (uncredited)
86 | National Guard (uncredited)
87 | Drill Sergeant (uncredited)
88 | Construction Worker (uncredited)
89 | NYPD (uncredited)
90 | Coffee Shop Employee (uncredited)
91 | Zen-Whoberi Elder (uncredited)
92 | Patron in Vehicle (uncredited)
93 | Nick Fury (uncredited)
94 | Jabari Warrior (uncredited)
95 | Citizen (uncredited)
96 | Asgardian (uncredited)
97 | NYC Pedestrian (uncredited)
98 | NYC Pedestrian (uncredited)
99 | Asgardian (uncredited)
100 | NYC Pedestrian (uncredited)
101 | Medical Assistant (uncredited)
102 | Business Worker (uncredited)
103 | Wounded Business Man (uncredited)
104 | Student (uncredited)
105 | NYC Pedestrian (uncredited)
106 | Dora Milaje (uncredited)
107 | Jack Rollins (uncredited)
108 | Boy on Bus (uncredited)
109 | Construction worker (uncredited)
110 | Pedestrian (uncredited)
111 | NYC Pedestrian / Phil (uncredited)
112 | NYC Pedestrian (uncredited)
113 | Asgardian (uncredited)
114 | Maria Hill (uncredited)
115 | NYC Pedestrian (uncredited)
116 | NYC Pedestrian (uncredited)
117 | New York Pedestrian (uncredited)
118 | New York Pedestrian (uncredited)
119 | NYC Maintenance (uncredited)
120 | Rando 1
121 | Rando 2
122 | ;
123 | run;
124 |
125 | ods noproctitle;
126 | /* "Comic" Sans -- get it???? */
127 | title font="Comic Sans MS"
128 | "Distribution of Oblivion (&algorithm. Algorithm)";
129 | ods graphics on / height=300 width=300;
130 | proc freq data=work.characters;
131 | table spared / plots=freqplot;
132 | run;
133 |
134 | /* Using an attribute map for data-driven symbols */
135 | data thanosmap;
136 | input id $ value $ markercolor $ markersymbol $;
137 | datalines;
138 | status 0 black frowny
139 | status 1 red heart
140 | ;
141 | run;
142 |
143 | title;
144 | ods graphics / height=400 width=400 imagemap=on;
145 | proc sgplot data=Characters noautolegend dattrmap=thanosmap;
146 | styleattrs wallcolor=white;
147 | scatter x=x y=y / markerattrs=(size=40)
148 | group=spared tip=(Name Spared) attrid=status;
149 | symbolchar name=heart char='2665'x;
150 | symbolchar name=frowny char='2639'x;
151 | xaxis integer display=(novalues) label="Did Thanos Kill You? Frowny=Dead"
152 | labelattrs=(family="Comic Sans MS" size=14pt);
153 |
154 | yaxis integer display=none;
155 | run;
156 |
157 | /* Conditional colors and strikethrough to indicate survival */
158 | title "Details of who was 'snapped' and who was spared";
159 | proc report data=Characters nowd;
160 | column Name spared;
161 | define spared / 'Spared' display;
162 | compute Spared;
163 | if spared=1 then
164 | call define(_row_,"style",
165 | "style={color=green}");
166 | if spared=0 then
167 | call define(_row_,"style",
168 | "style={color=red textdecoration=line_through}");
169 | endcomp;
170 | run;
171 | title;
--------------------------------------------------------------------------------
/rss-feeds/README.md:
--------------------------------------------------------------------------------
1 | See [Read RSS feeds with SAS using XML or JSON](https://blogs.sas.com/content/sasdummy/2019/04/11/read-rss-feeds/)
2 | for full explanation and context.
3 |
--------------------------------------------------------------------------------
/rss-feeds/blogspot-json.sas:
--------------------------------------------------------------------------------
1 | /* Copyright SAS Institute Inc. */
2 |
3 | /* Read JSON feed into a local file. */
4 | /* Use Blogspot parameters to get 100 posts at a time */
5 | filename resp temp;
6 | proc http
7 | url='https://googlemapsmania.blogspot.com/feeds/posts/default?alt=json&max-results=100'
8 | method="get"
9 | out=resp;
10 | run;
11 |
12 | libname rss json fileref=resp;
13 |
14 | /* Join the relevant feed entry items to make a single table */
15 | /* with post titles and URLs */
16 | proc sql;
17 | create table work.blogspot as
18 | select t2._t as rss_title,
19 | t1.href as rss_href
20 | from rss.entry_link t1
21 | inner join rss.entry_title t2 on (t1.ordinal_entry = t2.ordinal_entry)
22 | where t1.type = 'text/html' and t1.rel = 'alternate';
23 | quit;
24 |
25 | libname rss clear;
--------------------------------------------------------------------------------
/rss-feeds/wordpress-xml.sas:
--------------------------------------------------------------------------------
1 | /* Copyright SAS Institute Inc. */
2 |
3 | filename rssmap temp;
4 | data _null_;
5 | infile datalines;
6 | file rssmap;
7 | input;
8 | put _infile_;
9 | datalines;
10 |
11 |
12 |
13 |
14 |
15 | /rss/channel/item
16 |
17 | /rss/channel/item/title
18 | character
19 | string
20 | 250
21 |
22 |
23 | /rss/channel/item/link
24 | character
25 | string
26 | 200
27 |
28 |
29 | /rss/channel/item/pubDate
30 | character
31 | string
32 | 40
33 |
34 |
35 |
36 | ;
37 | run;
38 |
39 | /* WordPress feeds return data in pages, 25 entries at a time */
40 | /* So using a short macro to loop through past 5 pages, or 125 items */
41 | %macro getItems;
42 | %do i = 1 %to 5;
43 | filename feed temp;
44 | proc http
45 | method="get"
46 | url="https://www.starwars.com/news/feed?paged=&i."
47 | out=feed;
48 | run;
49 |
50 | libname result XMLv2 xmlfileref=feed xmlmap=rssmap;
51 |
52 | data posts_&i.;
53 | set result.item;
54 | run;
55 | %end;
56 | %mend;
57 |
58 | %getItems;
59 |
60 | /* Assemble all pages of entries */
61 | /* Cast the date field into a proper SAS date */
62 | /* Have to strip out the default day name abbreviation */
63 | /* "Wed, 10 Apr 2019 17:36:27 +0000" -> 10APR2019 */
64 | data allPosts ;
65 | set posts_:;
66 | length sasPubdate 8;
67 | sasPubdate = input( substr(pubDate,4),anydtdtm.);
68 | format sasPubdate dtdate9.;
69 | drop pubDate;
70 | run;
--------------------------------------------------------------------------------
/splitfile/splitfile.sas:
--------------------------------------------------------------------------------
1 | /* Reliable way to check whether a macro value is empty/blank */
2 | %macro isBlank(param);
3 | %sysevalf(%superq(param)=,boolean)
4 | %mend;
5 |
6 | /* We need this function for large file uploads, to telegraph */
7 | /* the file size in the API. */
8 | /* Get the file size of a local file in bytes. */
9 | %macro getFileSize(localFile=);
10 | %local rc fid fidc;
11 | %local File_Size;
12 | %let rc=%sysfunc(filename(_lfile,&localFile));
13 | %let fid=%sysfunc(fopen(&_lfile));
14 | %let File_Size=%sysfunc(finfo(&fid,File Size (bytes)));
15 | %let fidc=%sysfunc(fclose(&fid));
16 | %let rc=%sysfunc(filename(_lfile));
17 | %sysevalf(&File_Size.)
18 | %mend;
19 |
20 | %macro splitFile(sourceFile=,
21 | maxSize=327680,
22 | metadataOut=,
23 | /* optional, will default to WORK */
24 | chunkLoc=);
25 |
26 | %local filesize maxSize numChunks buffsize ;
27 | %let buffsize = %sysfunc(min(&maxSize,4096));
28 | %let filesize = %getFileSize(localFile=&sourceFile.);
29 | %let numChunks = %sysfunc(ceil(%sysevalf( &filesize / &maxSize. )));
30 | %put NOTE: Splitting &sourceFile. into &numChunks parts;
31 |
32 | %if %isBlank(&chunkLoc.) %then %do;
33 | %let chunkLoc = %sysfunc(getoption(WORK));
34 | %end;
35 |
36 | /* This DATA step will do the chunking. */
37 | /* It's going to read the original file in segments sized to the buffer */
38 | /* It's going to write that content to new files up to the max size */
39 | /* of a "chunk", then it will move on to a new file in the sequence */
40 | /* All resulting files should be the size we specified for chunks */
41 | /* except for the last one, which will be a remnant */
42 | /* Along the way it will build a data set with the metadata for these */
43 | /* chunked files, including the file location and byte range info */
44 | /* that will be useful for APIs that need that later on */
45 | data &metadataOut.(keep=original originalsize chunkpath chunksize byterange);
46 | length
47 | filein 8 fileid 8 chunkno 8 currsize 8 buffIn 8 rec $ &buffsize fmtLength 8 outfmt $ 12
48 | bytescumulative 8
49 | /* These are the fields we'll store in output data set */
50 | original $ 250 originalsize 8 chunkpath $ 500 chunksize 8 byterange $ 50;
51 | original = "&sourceFile";
52 | originalsize = &filesize.;
53 | rc = filename('in',"&sourceFile.");
54 | filein = fopen('in','S',&buffsize.,'B');
55 | bytescumulative = 0;
56 | do chunkno = 1 to &numChunks.;
57 | currsize = 0;
58 | chunkpath = catt("&chunkLoc./chunk_",put(chunkno,z4.),".dat");
59 | rc = filename('out',chunkpath);
60 | fileid = fopen('out','O',&buffsize.,'B');
61 | do while ( fread(filein)=0 ) ;
62 | call missing(outfmt, rec);
63 | rc = fget(filein,rec, &buffsize.);
64 | buffIn = fcol(filein);
65 | if (buffIn - &buffsize) = 1 then do;
66 | currsize + &buffsize;
67 | fmtLength = &buffsize.;
68 | end;
69 | else do;
70 | currsize + (buffIn-1);
71 | fmtLength = (buffIn-1);
72 | end;
73 | /* write only the bytes we read, no padding */
74 | outfmt = cats("$char", fmtLength, ".");
75 | rcPut = fput(fileid, putc(rec, outfmt));
76 | rcWrite = fwrite(fileid);
77 | if (currsize >= &maxSize.) then leave;
78 | end;
79 | chunksize = currsize;
80 | bytescumulative + chunksize;
81 | byterange = cat("bytes ",bytescumulative-chunksize,"-",bytescumulative-1,"/",originalsize);
82 | output;
83 | rc = fclose(fileid);
84 | end;
85 | rc = fclose(filein);
86 | run;
87 | %mend;
--------------------------------------------------------------------------------