Wednesday, June 24, 2009
Wednesday, June 17, 2009
More complicated JavaScript string tokenizer – and Twitter Conversations
(I'm not sure when I started using the term tokenizer - "formatter" may be more common...)
I’ve experimented in the past with a C# string.Format() alternative that would allow property names as tokens rather than the numeric index based tokens that string.Format() uses. Hardly a unique approach, and many others did it better.
Here’s another first-draft ‘tokenizer’, this time in JavaScript:
String.prototype.format = function(tokens) { ///<summary> ///This is an extension method for strings, using string or numeric tokens (e.g. {foo} or {0}) to format the string. ///<summary> ///<param name="tokens">One or more replacement values ///if a single object is passed, expects to match tokens with object property names, ///if a single string, number or boolean, replaces any and all tokens with the string ///if multiple arguments are passed, replaces numeric tokens with the arguments, in the order passed ///</param> ///<returns>the string with matched tokens replaced</returns> var text = this; try { switch (arguments.length) { case 0: { return this; }; case 1: { switch (typeof tokens) { case "object": { //loop through all the properties in the object and replace tokens matching the names var token; for (token in tokens) { if (!tokens.hasOwnProperty(token) || typeof tokens[token] === 'function') { break; } //else text = text.replace(new RegExp("\\{" + token + "\\}", "gi"), tokens[token]) } return text; }; case "string": case "number": case "boolean": { return text.replace(/{[a-z0-9]*}/gi, tokens.toString()); }; default: return text; }; }; default: { //if multiple parameters, assume numeric tokens, where each number matches the argument position for (var i = 0; i < arguments.length; i++) { text = text.replace(new RegExp("\\{" + i + "\\}", "gi"), arguments[i].toString()); } return text; }; }; } catch (e) { return text; } };
The comment (in VS Intellisense format) is pretty self-explanatory, note that it doesn’t allow any special formatting as String.Format does, nor does it even support escaping {}‘s – in general it is quite crude.
That said, when used within it’s limitations it works - below are a couple of actual usage scenarios, both from my ongoing Twitter Conversations experiment:
var url = "http://search.twitter.com/search.json?q=from:{p1}+to:{p2}+OR+from:{p2}+to:{p1}&since={d1}&until={d2}&rpp=50".format({ p1: $("#p1").attr("value"), p2: $("#p2").attr("value"), d1: $("#d1alt").attr("value"), d2: $("#d2alt").attr("value") });
and
$.getJSON(url + "&callback=?", function(data) { $.each(data.results, function(i, result) { content = ' \ <p> \ <a href="http://twitter.com/{from_user}"><img src="{profile_image_url}" />{from_user}</a> \ (<a href="http://twitter.com/{from_user}/statuses/{id}">{created_at}</a>): \ {text} \ </p>'.format(result);
The working Twitter Conversations sample is here, as you can tell it’s really just a wrapper around the Twitter Search API.
Labels: experiment, javascript, mashup, me-me-me
Tuesday, June 16, 2009
Twitter Conversations: More fun with Yahoo Pipes
<Preface>
Lets imagine that you can’t actually do all of this directly in Twitter Search like this:
http://search.twitter.com/search?q=from:jaketapper+to:senjohnmccain+OR+from:senjohnmccain+to:jaketapper&since=2009-06-16&until=2009-06-16&rpp=50
</Preface>
(Now contrived) How To follows:
I stumbled upon an impromptu (or so they claim) twitter interview between Jake Tapper (@jaketapper) of ABC and Sen. John McCain (@senjohnmccain). Unfortunately, short of following both of them, it was hard to get a gist of the actual conversation – I tried the Conversation view on Twitter Search, and a couple of other services, like Tweader (appears broken) and Quotably (dead).
Twitter Search has an advanced search/allows parameters to specify searching for tweets from one person to another, but that only gives you half a conversation. And as far as I can tell, Twitter search doesn’t allow multiple from:/to: pairs. Enter Yahoo Pipes’ union module:
This module merges up to five separate sources of items into a single list of items.
Since Twitter Search results can be had in both Atom and RSS flavor, this means we’re good to go:
<interlude>
…and this is where I discovered that you could indeed do duplex conversation searches in twitter search, so the rest of this will be short…
</interlude>
See http://pipes.yahoo.com/austegard/twitterconversations; click View Source or Clone to play with it (requires Yahoo id).
Labels: errors, experiment, mashup, me-me-me, twitter, yahoo
Simple JavaScript string tokenizer
A crude String.Format equivalent in JavaScript -blatantly copied from frogsbrain
//from http://frogsbrain.wordpress.com/2007/04/28/javascript-stringformat-method/ String.format = function(text) { if (arguments.length > 1) { for (var i = 0; i < arguments.length - 1; i++) { text = text.replace(new RegExp("\\{" + i + "\\}", "gi"), arguments[i + 1]); } } return text; };
#twitcode version (130 characters!)
strf=function(t){a=arguments;if(a.length>1)for(i=0;i<a.length-1;i++)t=t.replace(new RegExp("\\{"+i+"\\}","gi"),a[i+1]);return t};
Labels: c#, experiment, javascript
Monday, June 15, 2009
3 steps to a better Tech Crunch (feed)
I know I’m not alone in having a lack of confidence in the journalistic ability of Michael Arrington’s employees over at TechCrunch – MG Siegler’s outrage over Microsoft’s latest (rather sad) marketing efforts is just the latest to draw annoyed comments. With 20-30 posts per day, it gets pretty time-consuming just trying to filter the good stories from the bad, since all that Google Reader gives you is the headline and the start of the body:
Wouldn’t it be nice to also know who wrote that headline so that you knew better how to interpret it? Well, you can do so quite easily, in three simple steps.
Step 1: Create a Yahoo Pipe
Go to http://pipes.yahoo.com/pipes/pipe.edit
Step 2: Configure your Pipe
Add the following modules:
- Sources – FetchFeed
- Operators – Loop
- String – StringBuilder (drag onto the Loop module)
Configure and connect as below, then save.
Step 3: Subscribe to your new feed:
Or, if all that’s too complicated, you can just subscribe to the Pipe I created:
http://pipes.yahoo.com/austegard/tcwa
PS! Here’s a filtered feed for Michael Arrington posts only:
Tuesday, June 09, 2009
Outlook's dumbest feature?
Why? Why? Why?
It seems 9 out of 10 times the line breaks are significant and SHOULDN’T be removed.
The feature has persisted for about a decade (if not longer – I don’t recall if it was part of 97 and 98); if you don’t know by now, this is how you remove it:
Tools – Options – Preferences – Email Options, Uncheck the Remove… checkbox.
Thursday, June 04, 2009
My contributions to SPExLib
I just discovered Wictor Wilen (et al)’s excellent CodePlex project: SharePoint Extensions Lib, which aims to simplify SharePoint development on .NET 3.5 SP1 and up.
Since I never took the time to figure out how exactly to contribute directly to a CodePlex project, I instead first did an ad-hoc contribution of my (or rather Rob Garrett’s really) SPlist.AddItemOptimized() method. I then quickly went through the methods that are there so far in SPExLib, and culled from my own library those that overlapped. Below is the filtered result: these are extension methods not currently in the SPExlib project (as of June 04 2009).
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.SharePoint; namespace SPExLib.SharePoint { #region Site Extensions /// <summary> /// SPSite Extension Methods /// </summary> public static class SiteExtensions { /// <summary> /// Gets a built in site field (column). /// </summary> /// <param name="site">The site.</param> /// <param name="builtInFieldId">The built in field id.</param> /// <returns>The Built In <see cref="SPField"/>.</returns> public static SPField GetBuiltInField(this SPSite site, Guid builtInFieldId) { return site.RootWeb.Fields[builtInFieldId]; } /// <summary> /// Gets a <see cref="List{SPList}"/> of all lists contained within the site collection, including those in the root web and all the sub sites. /// </summary> /// <param name="site">The site.</param> /// <returns>A <see cref="List{SPList}"/> containing all the lists.</returns> public static List<SPList> GetAllLists(this SPSite site) { List<SPList> allLists = new List<SPList>(); for (int i = 0; i < site.AllWebs.Count; i++) { using (SPWeb web = site.AllWebs[i]) { allLists.AddRange(web.Lists.Cast<SPList>()); } } return allLists; } } #endregion #region Web Extensions /// <summary> /// Extensions to the SPWeb class /// </summary> public static class WebExtensions { /// <summary> /// Gets the relative URL of the web. /// </summary> /// <param name="web">The web.</param> /// <returns>The Url of the web, relative to the site.</returns> public static string GetRelativeUrl(this SPWeb web) { string siteUrl = web.Site.Url; string webUrl = web.Url; if (webUrl.Contains(siteUrl)) return webUrl.Replace(siteUrl, ""); return null; } /// <summary> /// Gets the list from the list's URL, relative to the Web (which is the url used to create the list). /// </summary> /// <param name="web">The web.</param> /// <param name="listUrl">The list URL, relative to the web (equals the url used to create the list).</param> /// <returns>A <see cref="SPList"/> instance, or null, if the list was not found.</returns> public static SPList GetListFromListUrl(this SPWeb web, string listUrl) { SPList result = null; listUrl = string.Format("{0}/{1}", web.Url, listUrl); try { result = web.GetList(listUrl); } catch { } return result; } } #endregion #region SPList Extensions /// <summary> /// SPList Extension Methods /// </summary> public static class ListExtensions { /// <summary> /// Adds a content type to the list, optionally adding the content type fields to the default view for the list. /// </summary> /// <param name="list">The list.</param> /// <param name="contentType">The content type.</param> /// <param name="addColumnsToDefaultView">if set to <c>true</c> add columns to the list's default view.</param> /// <returns>The <see cref="SPContentType"/> instance.</returns> public static SPContentType AddContentType(this SPList list, SPContentType contentType, bool addColumnsToDefaultView) { if (contentType == null || list.ContentTypes.ContainsContentType(contentType.Id)) return contentType; contentType = list.ContentTypes.Add(contentType); if (addColumnsToDefaultView) { bool viewUpdated = false; SPView defaultView = list.DefaultView; //SPViewFieldCollection viewFields = list.DefaultView.ViewFields; //Add content type fields (but only those not hidden, and also not ContentType and Title) foreach (SPField field in contentType.Fields) { if (!defaultView.ViewFields.Contains(field.InternalName) && !field.Hidden && !contentType.FieldLinks[field.Id].Hidden && !"|Title|ContentType|Name|".Contains(field.InternalName) ) { defaultView.ViewFields.Add(field); viewUpdated = true; } } if (viewUpdated) { defaultView.DefaultView = true; defaultView.Update(); } } list.Update(); return contentType; } #endregion #region SPDocumentLibrary /// <summary> /// Adds the content type to the document library, optionally adding the content type fields to the default view for the list. /// </summary> /// <param name="documentLibrary">The document library.</param> /// <param name="contentType">The content type.</param> /// <param name="addColumnsToDefaultView">if set to <c>true</c> [add columns to default view].</param> /// <returns>The <see cref="SPContentType"/> instance.</returns> public static SPContentType AddContentType(this SPDocumentLibrary documentLibrary, SPContentType contentType, bool addColumnsToDefaultView) { return ((SPList)documentLibrary).AddContentType(contentType, addColumnsToDefaultView); } #endregion #region SPListCollection /// <summary> /// Creates a list based on the specified title, description, URL, Feature ID, template type, document template type, and options for displaying a link to the list in Quick Launch. /// Overloads and corrects the parameter types of the default Add method /// </summary> /// <param name="listCollection">The list collection.</param> /// <param name="title">The title.</param> /// <param name="description">The description.</param> /// <param name="url">The URL.</param> /// <param name="featureId">The feature id.</param> /// <param name="templateType">The template type.</param> /// <param name="docTemplateType">The doc template type.</param> /// <param name="quickLaunchOptions">The quick launch options.</param> /// <returns>A GUID that identifies the new list.</returns> public static Guid Add(this SPListCollection listCollection, string title, string description, string url, Guid featureId, SPListTemplateType templateType, int docTemplateType, SPListTemplate.QuickLaunchOptions quickLaunchOptions) { return listCollection.Add(title, description, url, featureId.ToString("B").ToUpper(), (int)templateType, docTemplateType.ToString(), quickLaunchOptions); } #endregion #region SPViewFieldCollection /// <summary> /// Determines whether the view field collection contains a field of the specified internal name. /// </summary> /// <param name="viewFieldCollection">The view field collection.</param> /// <param name="internalFieldName">Internal name of the field.</param> /// <returns> /// <c>true</c> if the view field collection contains the field; otherwise, <c>false</c>. /// </returns> public static bool Contains(this SPViewFieldCollection viewFieldCollection, string internalFieldName) { return viewFieldCollection.Cast<string>().Any(f => f == internalFieldName); } /// <summary> /// Adds a specified field to the collection. /// </summary> /// <param name="col">The collection.</param> /// <param name="fieldGuid">The field's Guid.</param> public static void Add(this SPViewFieldCollection col, Guid fieldGuid) { col.Add(col.View.ParentList.Fields[fieldGuid]); } /// <summary> /// Deletes the specified field from the collection. /// </summary> /// <param name="col">The collection.</param> /// <param name="fieldGuid">The field's Guid.</param> public static void Delete(this SPViewFieldCollection col, Guid fieldGuid) { col.Delete(col.View.ParentList.Fields[fieldGuid]); } } #endregion #region SPContentType (and related classes) extensions /// <summary> /// SPContentType and related class extensions /// </summary> public static class ContentTypeExtensions { #region SPFieldLinkCollection Extensions /// <summary> /// Adds a field to the <see cref="SPFieldLinkCollection"/>. /// </summary> /// <param name="fieldLinkCollection">The field link collection.</param> /// <param name="field">The field.</param> public static void AddField(this SPFieldLinkCollection fieldLinkCollection, SPField field) { fieldLinkCollection.Add(new SPFieldLink(field)); } /// <summary> /// Clears the fields in a <see cref="SPFieldLinkCollection"/>. /// </summary> /// <param name="fieldLinkCollection">The field link collection.</param> public static void ClearFields(this SPFieldLinkCollection fieldLinkCollection) { for (int i = fieldLinkCollection.Count - 1; i >= 0; i--) { fieldLinkCollection.Delete(fieldLinkCollection[i].Id); } } #endregion #region SPContentType Extensions /// <summary> /// Adds a field link to the <see cref="SPContentType"/>. /// </summary> /// <param name="contentType">The content type.</param> /// <param name="field">The field.</param> public static void AddFieldLink(this SPContentType contentType, SPField field) { contentType.FieldLinks.Add(new SPFieldLink(field)); } /// <summary> /// First clears the field links collection from the content type, then re-adds all Parent content type field links. /// </summary> /// <param name="contentType">The content type.</param> public static void ResetFieldLinks(this SPContentType contentType) { contentType.FieldLinks.ClearFields(); foreach (SPFieldLink fl in contentType.Parent.FieldLinks) { contentType.FieldLinks.Add(fl); } } /// <summary> /// Clears the event receivers from the content type. /// </summary> /// <param name="contentType">The content type.</param> public static void ResetEventReceivers(this SPContentType contentType) { for (int i = contentType.EventReceivers.Count - 1; i >= 0; i--) { contentType.EventReceivers[i].Delete(); } //TODO: Does a content type inherit it's parent content type receiver? //contentType.EventReceivers = contentType.Parent.EventReceivers; } #endregion #region SPContentTypeCollection Extensions /// <summary> /// Determines whether the <see cref="SPContentTypeCollection"/> contains a content type with the specified name. /// </summary> /// <param name="contentTypeCollection">The content type collection.</param> /// <param name="name">The name.</param> /// <returns> /// <c>true</c> if the content type collection contains a content type with the specified name; otherwise, <c>false</c>. /// </returns> public static bool Contains(this SPContentTypeCollection contentTypeCollection, string name) { //return contentTypeCollection.Cast<SPContentType>().Any(ct => ct.Name == name); return (contentTypeCollection[name] != null); } /// <summary> /// Determines whether the content type collection contains a content type with the specified <see cref="SPContentTypeId"/>. /// </summary> /// <param name="contentTypeCollection">The content type collection.</param> /// <param name="contentTypeID">The content type ID.</param> /// <returns> /// <c>true</c> if the content type collection contains a content type with the specified <see cref="SPContentTypeId"/>; otherwise, <c>false</c>. /// </returns> public static bool ContainsContentType(this SPContentTypeCollection contentTypeCollection, SPContentTypeId contentTypeID) { //old way //for (int i=0; i < contentTypeCollection.Count; i++) //{ // if (contentTypeCollection[i].Id == contentTypeID) // return true; //} //return false; //new cool way: //return contentTypeCollection.Cast<SPContentType>().Any(ct => ct.Id == contentTypeID); //simple way return (contentTypeCollection[contentTypeID] != null); } /// <summary> /// Deletes a sealed content type by unsealing and un-readonly-ing the content type before attempting to delete it. /// </summary> /// <param name="contentTypeCollection">The content type collection.</param> /// <param name="name">The name.</param> /// <returns></returns> public static bool DeleteSealedContentType(this SPContentTypeCollection contentTypeCollection, string name) { bool success = false; try { SPContentType contentType = contentTypeCollection[name]; contentType.Sealed = false; contentType.ReadOnly = false; contentTypeCollection.Delete(contentType.Id); success = !contentTypeCollection.ContainsContentType(contentType.Id); } catch { } return success; } #endregion } #endregion /// <summary> /// Extension methods for SPFile /// </summary> public static class SPFileExtensions { /// <summary> /// Gets the file size as string. /// </summary> /// <param name="file">The file.</param> /// <returns></returns> public static string GetFileSizeAsString(this SPFile file) { double s = file.Length; string[] format = new string[] { "{0} bytes", "{0} KB", "{0} MB", "{0} GB", "{0} TB", "{0} PB", "{0} EB", "{0} ZB", "{0} YB" }; int i = 0; while (i < format.Length - 1 && s >= 1024) { s = (int)(100 * s / 1024) / 100.0; i++; } return string.Format(format[i], s.ToString("###,###,###.##")); } } }
Labels: c#, howto, me-me-me, moss, sharepoint