<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Case Else: &#187; Uncategorized</title>
	<atom:link href="http://caseelse.net/category/uncategorized/feed/" rel="self" type="application/rss+xml" />
	<link>http://caseelse.net</link>
	<description>/*** Code's last stand ***/</description>
	<lastBuildDate>Sat, 29 Aug 2009 20:41:08 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0</generator>
		<item>
		<title>Matching Arbitrary Name Lists in Active Directory</title>
		<link>http://caseelse.net/2008/09/14/matching-arbitrary-name-lists-in-active-directory/</link>
		<comments>http://caseelse.net/2008/09/14/matching-arbitrary-name-lists-in-active-directory/#comments</comments>
		<pubDate>Sun, 14 Sep 2008 21:57:55 +0000</pubDate>
		<dc:creator>Neil</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://caseelse.net/?p=49</guid>
		<description><![CDATA[This script will take a CSV file of two columns — ostensibly in &#8216;last-name, first-name&#8217; format — and attempt to convert it into valid userIDs, despite irregularities in the list. I frequently receive requests to automate the retrieval of user IDs for a list of arbitrary names, such as from Active Directory. The name list [...]]]></description>
			<content:encoded><![CDATA[<p>This <img class="size-medium wp-image-16" style="float: right; margin-left:10;margin-top:0; display:inline;" title="Hello, My Name Is sticker" src="http://caseelse.net/wp-content/uploads/2008/hellomynameis210.png" alt="Hello, My Name is sticker" />script will take a CSV file of two columns — ostensibly in &#8216;last-name, first-name&#8217; format — and attempt to convert it into valid userIDs, despite irregularities in the list.</p>
<p><span id="more-49"></span><br />
I frequently receive requests to automate the retrieval of user IDs for a list of arbitrary names, such as from Active Directory. The name list largely consists of last names followed by first names, but some of the names are reversed. Some of the names are nicknames or initials.</p>
<p>In our example case, the names in Active Directory aren&#8217;t properly normalized either. Merging AD Domains from multiple, older networks often results in non-normalized data (or more specifically, differently normalized data). Names may be entered as proper names or nicknames, and nicknames may be notated in ad-hoc formats (e.g., in parentheses, etc). Names may be initialed—especially middle names—and abbreviations may or may not have periods. Last names may have suffixes (Jr, III, etc.) in a variety of formats.</p>
<p>Because the names in AD are not normalized, plus the arbitrary state of the names list we have to match, there is no way that I know of for a script to reliably normalize the list or the database, let alone compare them both. We could attempt to build-in dictionaries of names and compare them, but we would still fail on corner cases.</p>
<p>For example, if I tell you a full name is some arrangement of &#8216;Ernie&#8217;, &#8216;Ford&#8217;, and &#8216;Tennessee&#8217;, how can you guarantee what order the partial names are? While you may be able to make an educated guess, how do you write a script that guesses correctly? What about &#8216;John Olivia Newton&#8217;? Even if you built a dictionary of English names, what if I gave you &#8216;Ali Muhammad&#8217; with no specified order?</p>
<p>No matter how we attempt to automate matching these names to the data, there will still have to be a manual reckoning. The goal for the script is to make that manual process as simple and automatic as possible so that it can be done quickly, efficiently, and—most importantly—by somebody else. The goal isn&#8217;t to get 100% accuracy—80% would be nice with some lists. On the other hand, manually resolving 20% of a list of thousands of names is still a big job, but the 80% saving is huge.</p>
<p>If you have such a file, all you actually have to do is drop it onto this script, and the script will create an output file in the same folder. You can also pass the file name as a command-line parameter. The script doesn&#8217;t take extra parameters—the output file will be the input filename with &#8216;.out.tab&#8217; tacked onto it, etc.</p>
<p>Name dumps typically mix up first and last names, so this script tries to match names in both directions. The exception to that is the simplest translation. If &#8216;Smith, Andrew&#8217; is in the file, and matches to &#8216;Smith, Andrew&#8217; in AD, there is a strong indication (the comma) that the names are in the same order. That&#8217;s considered a quick match and warrants no further resolution.</p>
<p>If the simple translation fails, the script begins to run queries against AD, successively truncating the names (attempting to remove anything like initials, suffixes, etc.), and using them as wildcards until something is found, and eventually trying pieces of the names if necessary. The output includes profile information, as available, to help identify users.</p>
<p><strong>Caveats</strong></p>
<ul>
<li>Quick matches run through a fast translation (the AD NameTranslate object built into XP). There is some room for error; if the names have duplicates, for example (note Gene Cox in the list below). If this assumption of accuracy doesn&#8217;t work for your situation, you can disable that function (set it to just return an empty string).</li>
<li>There is no realistic way for a script to know that &#8216;Bob Smith&#8217; could be listed as &#8216;Robert Smith&#8217;, so it keeps broadening the net until something becomes plausible. If there is already a ‘B. Smith’ of some other name, broader scans will not run, so Robert may never show in the list.</li>
<li>Some queries (such as Bob Brown or Will Smith) may return too many names to be useful. There is a configurable threshold for that eventuality.</li>
</ul>
<p>You will notice that the output file is a .tab (which is probably an unrecognized type on your system) and that it looks oddly formatted. The file is designed so that you can run through the output fairly rapidly in a text editor and delete extraneous names, leaving the correct user IDs. Open the remainder in Excel (just right-click the file, and open it with Excel, in most cases), and all of the user IDs are in the third column—so even though editing is a manual process, it’s still relatively quick and easy.</p>
<p>Attached is a sample of the output, with mostly trouble names included.</p>
<pre class="brush: text">Wesley Walker	=	OurCorp\WWalker
Betn Gotham	=	OurCorp\BGotham
MacLarsen, William C. (Carlson) = ...
      {searching for &#039;MacLarsen&#039; &amp; &#039;William C. (Carlson)&#039;}
      Carlson MacLarsen (Carlson MacLarsen Project Office) (MAC Services - Office) =	CMacLars
Ferrous, B D = ...
      {searching for &#039;Ferrous&#039; &amp; &#039;B D&#039;}
      Ferrous, Brandon (Desktop Ops) =	BFerrou
      Ferrous, BD (Corporate Services) =	BDFerro
O&#039;Malley, Katherine = ...
      {searching for &#039;O&#039;&#039;Malley&#039; &amp; &#039;Katherine&#039;}
      Katherine O&#039;Malley (Company User Administration) (Company Administration) =	KOMalle
      Katherine O&#039;Malley (Network &amp; Server) =	KOMalle
Thistle, Bob = ...
      {searching for &#039;Thistle&#039; &amp; &#039;Bob&#039;}
      did not find &#039;B* Thistle&#039; or &#039;B* Bob&#039;
      Broadening Search...
      {searching for &#039;Thistle&#039;}
      Robert Thistle (Network &amp; Server Group) =	RThistle
      Thistle, Alex (Michael Robert) =	AThistle
      {searching for &#039;Bob&#039;}
      did not find &#039;* Bob&#039;
White, Clarence = ...
      {searching for &#039;White&#039; &amp; &#039;Clarence&#039;}
      Found 57 of &#039;C* White&#039; or &#039;B* Clarence&#039;. 30 is too many.
Garofolo, Gerald = ...
      {searching for &#039;Garofolo&#039; &amp; &#039;Gerald&#039;}
      Jerry Garofolo (Project Coordinator) (Business Support) =	JGarofol
Gene Cox      =     DOMAIN\ADMIN
Priestley Jr, Carl = ...
      {searching for &#039;Priestley Jr&#039; &amp; &#039;Carl&#039;}
      did not find &#039;C* Priestley Jr&#039; or &#039;C* Carl&#039;
      Broadening Search...
      {searching for &#039;Priestley Jr&#039;}
      did not find &#039;* Priestley Jr&#039;
      Trying just the first part (Priestley)
      {searching for &#039;Priestley*&#039;}
      Nick Priestley =	NPriestle
      Priestley, Carl (Legacy Support)  =	CPriestle
      {searching for &#039;Carl&#039;}
      Kelly Carl (HR) =	KCarl
      John Carl (HR) =	JCarl</pre>
<p>Below is the script:</p>
<pre class="brush: vb">Option Explicit

Const LDAPOU = &quot;dc=domain,dc=Company,dc=NET&quot;
Const TOOMANYCHOICES=30

Const ForReading = 1
Const ForWriting = 2

dim objArgs
Set objArgs = WScript.Arguments
If objArgs.count &lt; 1 Then
	wscript.echo &quot;Usage: userstocomputers.vbs input.csv&quot;
	wscript.quit
End If

Dim strInputFileName, strOutputFileName

strInputFileName = objArgs(0)
strOutputFileName=strInputFileName &amp; &quot;.tab&quot;

Dim objFSO, objInputFile, objOutputFile, szScratch, szScratch1
Set objFSO = CREATEOBJECT(&quot;Scripting.FileSystemObject&quot;)
Set objInputFile = objFSO.OpenTextFile(strInputFileName,ForReading,false)
Set objOutputFile = objFSO.OpenTextFile(strOutputFileName,ForWriting,true)

Dim strLineText, objSystemSet, objSystem
While not objInputFile.AtEndOfStream

	strLineText = split(Trim(objInputFile.Readline), &quot;,&quot;)
	if ubound(strLineText) &gt; 0 then
		strLineText(0)=trim(strLineText(0))
		strLineText(1)=trim(strLineText(1))
		szScratch= strLineText(1) &amp; &quot; &quot; &amp; strLineText(0)
		szScratch1=TranslateADName(4, 3, szScratch)
		if len(szScratch1) &gt; 0 then
			objOutputFile.WriteLine szScratch &amp; vbtab &amp; &quot;=&quot; &amp; vbtab &amp; szScratch1
		else
			szScratch= strLineText(0) &amp; &quot;, &quot; &amp; strLineText(1)
			szScratch1=TranslateADName(4, 3, szScratch)
			if len(szScratch1) &gt; 0 then
				objOutputFile.WriteLine szScratch &amp; vbtab &amp; &quot;=&quot; &amp; vbtab &amp; szScratch1
			else
				objOutputFile.WriteLine szScratch &amp; &quot; = ...&quot;

				if LookupADName( strLineText(0), strLineText(1) )=0 then
					objOutputFile.WriteLine vbtab &amp; &quot;Broadening Search...&quot;
					if LookupADName(strLineText(0), &quot;&quot;)=0 then
						if instr(strLineText(0), &quot; &quot;)&gt;0 then
							objOutputFile.WriteLine vbtab &amp; &quot;Trying just the first part (&quot; &amp; left(strLineText(0),instr(strLineText(0), &quot; &quot;)-1) &amp; &quot;)&quot;
							LookupADName left(strLineText(0),instr(strLineText(0), &quot; &quot;)-1)&amp;&quot;*&quot;, &quot;&quot;
						end if
					end if
					if LookupADName(strLineText(1), &quot;&quot;)=0 then
						if instr(strLineText(1), &quot; &quot;)&gt;0 then
							objOutputFile.WriteLine vbtab &amp; &quot;Trying just the first part (&quot; &amp; left(strLineText(1),instr(strLineText(1), &quot; &quot;)-1) &amp; &quot;)&quot;
							LookupADName left(strLineText(1),instr(strLineText(1), &quot; &quot;)-1)&amp;&quot;*&quot;, &quot;&quot;
						end if
					end if
				end if
			end if
		end if
	elseif ubound(strLineText) = 0 then
		strLineText(0)=trim(strLineText(0))
		szScratch1=strLineText(0)  &amp; vbtab &amp; &quot;=&quot; &amp; vbtab &amp; TranslateADName(4, 3, strLineText(0) )
			if len(szScratch1) &gt; 0 then
				objOutputFile.WriteLine szScratch &amp; vbtab &amp; &quot;=&quot; &amp; vbtab &amp; szScratch1
			else
				objOutputFile.WriteLine szScratch &amp; &quot; = ...&quot;
				LookupADName strLineText(0), &quot;&quot;
			end if
	end if
Wend
Wscript.Echo &quot;Done&quot;

Function TranslateADName(FromType, ToType, FromName)
	Dim objTranslator
	Const ADS_NAME_INITTYPE_GC					= 3

	Const   ADS_NAME_TYPE_1779					= 1
	Const   ADS_NAME_TYPE_CANONICAL				= 2
	Const   ADS_NAME_TYPE_NT4					= 3
	Const   ADS_NAME_TYPE_DISPLAY					= 4
	Const   ADS_NAME_TYPE_DOMAIN_SIMPLE			= 5
	Const   ADS_NAME_TYPE_ENTERPRISE_SIMPLE			= 6
	Const   ADS_NAME_TYPE_GUID					= 7
	Const   ADS_NAME_TYPE_UNKNOWN				= 8
	Const   ADS_NAME_TYPE_USER_PRINCIPAL_NAME		= 9
	Const   ADS_NAME_TYPE_CANONICAL_EX				= 10
	Const   ADS_NAME_TYPE_SERVICE_PRINCIPAL_NAME		= 11
	Const   ADS_NAME_TYPE_SID_OR_SID_HISTORY_NAME	= 12 

	Set objTranslator = CreateObject(&quot;NameTranslate&quot;)

	on error resume next
	objTranslator.Init ADS_NAME_INITTYPE_GC, &quot;&quot;
	objTranslator.Set FromType, FromName

	TranslateADName = objTranslator.Get(ToType)
	on error goto 0
End Function

Function LookupADName(fname, lname) &#039; Returns number of records
	dim objConnection, objCommand, objRecordSet, searchscope, strItem
	On Error Resume Next
	Const ADS_SCOPE_SUBTREE = 2

	fname=trim(replace(fname, &quot;&#039;&quot;, &quot;&#039;&#039;&quot;))
	lname=trim(replace(lname, &quot;&#039;&quot;, &quot;&#039;&#039;&quot;))
	if len(fname)=0 and len(lname)=0 then exit function

	Set objConnection = CreateObject(&quot;ADODB.Connection&quot;)
	Set objCommand =   CreateObject(&quot;ADODB.Command&quot;)
	objConnection.Provider = &quot;ADsDSOObject&quot;
	objConnection.Open &quot;Active Directory Provider&quot;
	Set objCommand.ActiveConnection = objConnection

	objCommand.Properties(&quot;Page Size&quot;) = 1000
	objCommand.Properties(&quot;Searchscope&quot;) = ADS_SCOPE_SUBTREE
	if len(fname)=0 or len(lname)=0 then
		lname = fname &amp; lname
		if lname = &quot;*&quot; then
			objOutputFile.WriteLine vbtab &amp; &quot;Skipping &#039;*&#039;&quot;
			exit function
		end if
		objOutputFile.WriteLine vbtab &amp; &quot;{searching for &#039;&quot; &amp; lname &amp; &quot;&#039;}&quot;
		objCommand.CommandText = &quot;SELECT sAMAccountName, displayName, department, description FROM &#039;LDAP://&quot; &amp; LDAPOU &amp; &quot;&#039; WHERE objectCategory=&#039;user&#039; AND &quot; &amp; _
			&quot;sn=&#039;&quot; &amp; lname &amp; &quot;&#039;&quot;
		searchscope=&quot;&#039;* &quot; &amp; lname &amp; &quot;&#039;&quot;
	else
		objOutputFile.WriteLine vbtab &amp; &quot;{searching for &#039;&quot; &amp; fname &amp; &quot;&#039; &amp; &#039;&quot; &amp; lname &amp; &quot;&#039;}&quot;
		objCommand.CommandText = &quot;SELECT sAMAccountName, displayName, department, description FROM &#039;LDAP://&quot; &amp; LDAPOU &amp; &quot;&#039; WHERE objectCategory=&#039;user&#039; AND &quot; &amp; _
			&quot;( (givenName=&#039;&quot; &amp; left(lname,1) &amp; &quot;*&#039; AND sn=&#039;&quot; &amp; fname &amp; &quot;&#039;) OR &quot; &amp; _
			&quot;(givenName=&#039;&quot; &amp; left(fname,1) &amp; &quot;*&#039; AND sn=&#039;&quot; &amp; lname &amp; &quot;&#039;) )&quot;
		searchscope=&quot;&#039;&quot; &amp; left(lname,1) &amp; &quot;* &quot; &amp; fname &amp; &quot;&#039; or &#039;&quot; &amp; left(fname,1) &amp; &quot;* &quot; &amp; lname &amp; &quot;&#039;&quot;
	end if
	Set objRecordSet = objCommand.Execute
	objRecordSet.MoveFirst
	LookupADName = objRecordSet.RecordCount
	if objRecordSet.RecordCount&gt;=TOOMANYCHOICES then
		objOutputFile.WriteLine vbtab &amp; &quot;Found &quot; &amp; objRecordSet.RecordCount &amp; &quot; of &quot; &amp; searchscope &amp; &quot;. &quot; &amp; TOOMANYCHOICES &amp; &quot; is too many.&quot;
	elseif objRecordSet.RecordCount=0 then
		objOutputFile.WriteLine vbtab &amp; &quot;did not find &quot; &amp; searchscope
	else
		Do Until objRecordSet.EOF
			objOutputFile.Write vbtab &amp; objRecordSet.Fields(&quot;displayName&quot;).Value
			if not isnull(objRecordSet.Fields(&quot;description&quot;).Value) then
				For each strItem in objRecordSet.Fields(&quot;description&quot;).Value
					if len (strItem)&gt;0 then objOutputFile.Write  &quot; (&quot; &amp; strItem &amp; &quot;)&quot;
				Next
			end if
			if len(objRecordSet.Fields(&quot;department&quot;).Value)&gt;0 then objOutputFile.Write  &quot; (&quot; &amp; objRecordSet.Fields(&quot;department&quot;).Value &amp; &quot;)&quot;
			objOutputFile.WriteLine  &quot; =&quot;&amp; vbtab &amp; objRecordSet.Fields(&quot;sAMAccountName&quot;).Value
			objRecordSet.MoveNext
		Loop
	end if
	on error goto 0
End Function</pre>
]]></content:encoded>
			<wfw:commentRss>http://caseelse.net/2008/09/14/matching-arbitrary-name-lists-in-active-directory/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Why not to use Excel for Data Gathering</title>
		<link>http://caseelse.net/2008/08/06/why-not-to-use-excel-for-data-gathering/</link>
		<comments>http://caseelse.net/2008/08/06/why-not-to-use-excel-for-data-gathering/#comments</comments>
		<pubDate>Wed, 06 Aug 2008 05:05:42 +0000</pubDate>
		<dc:creator>neil</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://caseelse.net/?p=36</guid>
		<description><![CDATA[Here&#8217;s a request that happens fairly regularly in different offices I&#8217;ve been associated with: We receive surveys/orders/documents that have been filled out by various people as Excel spreadsheets We need to put the information into a database We want you to write a script to automate the imports. My thoughts as a scripter: This is [...]]]></description>
			<content:encoded><![CDATA[<p>Here&#8217;s a request that happens fairly regularly in different offices I&#8217;ve been associated with:</p>
<blockquote>
<ol>
<li>We receive surveys/orders/documents that have been filled out by various people as Excel spreadsheets</li>
<li>We need to put the information into a database</li>
<li>We want you to write a script to automate the imports.</li>
</ol>
</blockquote>
<p>My thoughts as a scripter:<span id="more-36"></span></p>
<ol>
<li>This is an excellent way to keep a contractor in a job for a long time.</li>
<li>You will <strong>never</strong> get everybody to input the data the same way into Excel. Nobody <strong>ever</strong> has.
<ul>
<li>People will put things on different lines, because it is Excel, and they can.</li>
<li>People will never enter data the same way. If you give them a yes/no column in a spreadsheet, some time you will get &#8220;yes/no&#8221;, sometimes &#8220;y/n&#8221;, &#8220;t,f&#8221;, &#8220;X, &#8221;, &#8220;☑,☒&#8221;, &#8220;☺,☹&#8221;, &#8220;+,-&#8221;. Every time somebody creates a new item, the script breaks, and has to be modified to compensate, or you have to send the form back or correct it. With a web form, you just give them a yes/no box, or a check box, and they can&#8217;t put in different values (it&#8217;s self-correcting).</li>
<li>Whenever the user changes the values, if you haven&#8217;t predicted what they will enter, you will have to manually redo those files, because they will break the script. This <strong>always</strong> happens. Then you have to go back and fix the script.</li>
</ul>
</li>
<li>If you want to run the script on a server, you either have to work out how to dissect XLS files, or install Excel on the server. The first is a royal pain, and the second shouldn&#8217;t be allowed to happen.</li>
</ol>
<p>The best way to deal with this is to avoid it altogether:</p>
<ol>
<li>Get your database in a SQL database back end (and don&#8217;t confuse Excel with a database.)</li>
<li>Set up a web form that looks like the spreadsheet, to do data input straight into the database</li>
<li>Set up list boxes, etc, so that the data is picked, and not created on the fly.</li>
<li>Instead of filling out the spreadsheet, users just fill out a web page.</li>
<li>If you really want, you can send out Access-via-Outlook forms to anybody with Outlook. They are like Access forms, except they get emailed, and they send the data straight into a database.</li>
</ol>
<p>Miscellaneous advantages to web form:</p>
<ol>
<li>It takes almost all of the work out</li>
<li>You don&#8217;t have to correct data for people. If there&#8217;s a problem, it can let them know as they enter it, and they can correct it on the spot.</li>
<li>You don&#8217;t have to worry about different versions of Excel and interoperability.</li>
<li>You don&#8217;t even have to worry if people <strong>have</strong> Excel.</li>
<li>People can fill in a web form from any machine&#8211;PC, Macintosh, Blackberry.</li>
<li>If you change things, the web form changes along with the back end, and anybody going to the form automatically gets the new version&#8211;with Excel, you get back what you sent out any time ago.</li>
</ol>
<p>Doing this as a script will cost you a lot of time and effort (which probably translates into money), and will never work right (which will cost you a lot more money). Doing this as a web form will be relatively easy (and cheap), and the data will be self-correcting. If you want to do Access-via-Outlook forms, you will have plenty of money left over, in case you need to hire somebody to create that (it shouldn&#8217;t be hard, nor take long to make.) Besides, starting with a website, and just having an input form added to it is barely more work than the website by itself&#8211;and whoever builds the website will probably build the form as they build the site, anyway, just to do test data (I always do, when I build databases.)</p>
<p>If you still insist on doing this as a script, do it in Perl. Perl has modules to parse XLS files (so it doesn&#8217;t need Excel) and is built to parse native language text&#8211;which it will have to do for anything people can type in wrong&#8211;and can compile into an EXE, if you need to run it on a server without installing a new language. If you don&#8217;t need to run it on a server, Perl is still the best language to do it in. It&#8217;d still be an expensive and troublesome pain, but not as much as other scripting languages.</p>
<p>This kind of project is a knock-off for a web-developer. It&#8217;s a career for a scripter.</p>
]]></content:encoded>
			<wfw:commentRss>http://caseelse.net/2008/08/06/why-not-to-use-excel-for-data-gathering/feed/</wfw:commentRss>
		<slash:comments>16</slash:comments>
		</item>
	</channel>
</rss>
